[C++] 12 Qt5 GUIアプリ作成 QMainWindow

[M1 Mac, Big Sur 11.6.5]

Qt5でデプロイの目処が立ったので、ようやくPyQt6からの移植に着手しました。

さすが古株のウィジェットツールキットだけあってかなり前からの記事が検索でヒットしました。ただ記事の日付やバージョンを明記していないため、陳腐化した情報に振り回されることもありました。

自分もそうですがせめて記事の日付やバージョン情報をきちんと書いておかないと後進の役に立たないです。

まずはQMainWindowの座標とサイズ、背景色を設定しました。

#include <QtWidgets/QApplication>
#include <QtWidgets/QLabel>
#include <QtWidgets/QWidget>
#include <QMainWindow>

int main(int argc, char *argv[]){
    QApplication a(argc, argv);

    QMainWindow *mainWin = new QMainWindow();
    mainWin->setGeometry(100,100,360,220);
    mainWin->setWindowTitle("IMAGE INSPECTOR");
    mainWin->setStyleSheet("QMainWindow {background: '#708090';}");
    
    mainWin->show();
    return a.exec();
}

[C++] 11 Qt5 appファイル作成&デプロイ qmake

[M1 Mac, Big Sur 11.6.5]

Qt6のCMakeがまだよく分からないので、Qt5にダウングレードしてデプロイしました。Hello WorldレベルであればApple Siliconでも問題は発生しませんでした。

Java(Oracle)はノンIDE派にも優しくてユーザーへの負担が少なくなるように配慮されている印象ですが、最新のQt6については高度化が著しい上に情報が少なく、専用IDEであるQt Creatorを使わないと厳しい感じがします。いずれノンIDEで挑戦したいです。

手順は以下の通りです。

1.Qtの公式サイトに登録し、Qt5.15.2(27.2GB)をダウンロード&インストールする。

2.Qtのバイナリがあるところへパスを通す。今回は優先順位を最上位にしました。

export PATH=[HOME]/Qt/5.15.2/clang_64/bin:$PATH

3.プロジェクトディレクトリをカレントディレクトリにして、以下コマンドでMakefileとproファイルを作成する。

qmake -project && qmake

4.アイコンを設定するため、proファイルに以下内容を追記する。

# Qt5以降用
QT+=widgets

# アイコン設定
ICON = images/ImageInspector.icns
RESOURCE_FILES.files = $$ICON
RESOURCE_FILES.path = Contents/Resources
QMAKE_BUNDLE_DATA += RESOURCE_FILES

5.makeコマンドでappファイルを作成する。

6.以下コマンドでアプリをデプロイする。
macdeployqtコマンドへのパスは通っているはずなのにエラーになり、仕方なく絶対パスを使いました。サイズはHello Worldでも40MBになりました。PyQt6よりもかなり小さくなったものの、まだ大きいです。

[HOME]/Qt/5.15.2/clang_64/bin/macdeployqt ImageInspector.app/

[C++] 10 Qt6 CMakeによるビルド

[M1 Mac, Big Sur 11.6.5]

リリース用appファイルの作成方法を調べていくうちに、Qt6ではqmakeではなくCmakeというビルド自動化ソフトの使用が推奨されていることが判明しました。

せっかくMakefileを使えるようになったのにまた難物が登場しました。

とりあえず実行ファイルを作成するところまでできたので記録しておきます。

1.CMakeを公式サイトからダウンロードしてインストールする。CMakeはGUIソフトですが、Contents内にあるバイナリを直接使います。

2.CMakeバイナリのあるところにパスを通す。

export PATH=$PATH:/Applications/CMake.app/Contents/bin

3.CMakeLists.txtを作成しプロジェクト直下に置く。

cmake_minimum_required(VERSION 3.16)

project(ImageInspector VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

find_package(Qt6 REQUIRED COMPONENTS Widgets)

add_executable(ImageInspector
    ImageInspector.cpp
)

target_link_libraries(ImageInspector PRIVATE Qt6::Widgets)

4.プロジェクト内にbuildディレクトリを作成し、カレントディレクトリをそこに移す。

5.以下コマンドでMakefileを作成する。

cmake ..

6.makeコマンドで実行ファイルを作成する。

[C++] 09 Qt6のappファイル作成 qmake

[M1 Mac, Big Sur 11.6.5]

Qt6アプリのappファイルをテスト作成しました。複雑なアプリは作らないので専用IDEのQt Creatorは使わないつもりです。

基本的にネット情報を参考に進めました。方法は以下の通りです。デフォルトでアイコンの設定がないのは意外でした。

1. HomebrewでQt6をインストールし、バイナリのあるところにパスを通す。

export PATH=$PATH:/opt/homebrew/Cellar/qt/6.2.3_1/bin

2.プロジェクトを作成する。icnsファイルはimagesディレクトリに置く。

3.プロジェクトディレクトリをカレントディレクトリにして、以下コマンドでMakefileとproファイルを作成する。

qmake -project && qmake

4.アイコンを設定するため、proファイルに以下内容を追記する。

# Qt6用
QT+=widgets

# アイコン設定
ICON = images/ImageInspector.icns
RESOURCE_FILES.files = $$ICON
RESOURCE_FILES.path = Contents/Resources
QMAKE_BUNDLE_DATA += RESOURCE_FILES

5.makeコマンドでappファイルを作成する。

6.appファイル内Contents直下のInfo.plistに以下内容を追記する。

<key>CFBundleIconFile</key>
<string>ImageInspector.icns</string>

[Python]329 PyQt6 ボタン動作の実装

[M1 Mac, Big Sur 11.6.5, Python 3.10.0]

前回のコードにボタン動作を追加しPyQt6アプリを完成させました。icnsファイル作成箇所はクラスファイルとして別にしました。このアプリではドラッグ&ドロップでファイルパスを取得することもできます。

appファイルにすると260MBの巨大サイズになりました。さくっと作れますがやはりファイルの大きさが問題です。

アプリはとりあえず自サイトにアップしました。ここからC++の本家Qtに移植の予定です。

PyQt6はこれまで見てきたウィジェットツールキットの中では最も扱いやすかったです。並べると、PyQt6 > Swing >>> Tkinter >>FLTK といったところでしょうか。上位2つは企業開発であり、商品としてそれなりに洗練されているということでしょう。

FLTKは外観・文法・ウィジェットの名称など、クセが強すぎて開発意欲が低いままです。ラジオボタンがFl_Round_Button、ラベルがFl_Boxなどなどネーミングがピンとこなくて困っています。

2022/03/25追記
pngからjpgに変換しなくても解像度を上げられましたので後日コードをアップします。

https://www.tampiweb.site/?page_id=453
import sys
from PyQt6.QtWidgets import QLabel,QWidget,QApplication,QTextEdit,QLineEdit,QPushButton,QButtonGroup,QRadioButton
from PyQt6.QtCore import Qt
from pathlib import Path
from PIL import Image
import makeIcns

class ImageInspector(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("IMAGE INSPECTOR")
        self.setGeometry(100,100,360,220)
        self.setStyleSheet('background-color: #708090')
        self.setAcceptDrops(True)
        
        file = QLabel('File',self)
        file.setGeometry(15,15,26,16)
        file.setAlignment(Qt.AlignmentFlag.AlignCenter)
        
        self.input= QLineEdit('',self)
        self.input.setGeometry(50,10,220,25)
        self.input.setAcceptDrops(True)
        
        execution = QPushButton('実行',self)
        execution.setGeometry(290,10,50,30)
        execution.released.connect(self.execute)
        
        clear = QPushButton('クリア',self)
        clear.setGeometry(290,50,50,30)
        clear.released.connect(self.clear)
        
        self.rbtns = QButtonGroup()
        self.inspect = QRadioButton("Inspect",self)
        self.inspect.setGeometry(50,40,90,20)
        self.inspect.setChecked(True)
        self.rbtns.addButton(self.inspect)
        
        self.resize_img = QRadioButton("Resize",self)
        self.resize_img.setGeometry(50,65,90,20)
        self.rbtns.addButton(self.resize_img)
        
        width_label = QLabel('W',self)
        width_label.setGeometry(135,70,15,10)
        width_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        width_label.setStyleSheet('font-size:10px')
        
        self.width = QLineEdit('',self)
        self.width.setGeometry(155,65,45,20)
        
        height_label = QLabel('H',self)
        height_label.setGeometry(205,70,15,10)
        height_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        height_label.setStyleSheet('font-size:10px')
        
        self.height = QLineEdit('',self)
        self.height.setGeometry(220,65,45,20)
        
        self.icns = QRadioButton("icns作成",self)
        self.icns.setGeometry(50,90,90,20)
        self.icns.setToolTip('PNG file[2048*2048,72px] required')  
        self.rbtns.addButton(self.icns)
        
        self.output = QTextEdit('',self)
        self.output.setGeometry(50,115,240,100)
        self.output.setAcceptDrops(False)
        
    def dragEnterEvent(self, e):
        if(e.mimeData().hasUrls()):
            e.accept()

    def dropEvent(self, e):
        urls = e.mimeData().urls()
        path = Path(urls[0].toLocalFile())
        print(f"Drop_path {path}")
        self.input.setText(str(path))
        
    def execute(self):
        image_path = self.input.text()
        print(f"Exe_path {image_path}")
        img = Image.open(image_path)
        
        if self.inspect.isChecked():
            self.output.append("Size " + str(img.size) + "\n"+ "dpi " + str(img.info.get('dpi')))
        elif self.resize_img.isChecked():
            width = int(self.width.text())
            height = int(self.height.text())
            resize_img= img.resize((width,height))
            resize_img_path = ".".join(self.input.text().split(".")[:-1]) + "_resized." + (self.input.text()).split(".")[-1]
            print(f"Resize_path {resize_img_path}")
            resize_img.save(resize_img_path)
        else: # icns作成
            makeIcns.MakeIcns.make(None,image_path,self)
        
    def clear(self):
        self.input.clear()
from PIL import Image
import subprocess, os
from PyQt6.QtWidgets import QDialog,QPushButton,QLabel
from PyQt6.QtCore import Qt

class MakeIcns():
    def make(self, filepath0, window):
        img = Image.open(filepath0)
        print(str(img.size))
        if str(img.size)=="(2048, 2048)": # 2048*2048
            filepath0_jpg = ".".join(filepath0.split(".")[:-1]) + ".jpg" # 1024*1024 dpi=144
            filepath = ".".join(filepath0.split(".")[:-1]) + "1.png" # 1024*1024 dpi=144
            filepath2 = ".".join(filepath0.split(".")[:-1]) + "2.png" # 512*512 dpi=72
            filedir = "/".join(filepath0.split("/")[:-1]) + "/" + (filepath0.split("/")[-1]).split(".")[-2] + ".iconset/"
            
            print(f"filepath0_jpg {filepath0_jpg}")
            print(f"filepath {filepath}")
            print(f"filepath2 {filepath2}")
            print(f"filedir {filedir}")
            
            os.mkdir(filedir)

            img1 = Image.open(filepath0).convert("RGB")
            # 512*512 dpi=72
            img1_resize = img1.resize((512,512))
            img1_resize.save(filepath2)

            # dpi=144にするため一旦jpgへ変換
            img1_resize2 = img1.resize((1024,1024))
            img1_resize2.save(filepath0_jpg,dpi=(144,144))
            # jpgをpngへ変換
            img3 = Image.open(filepath0_jpg).convert("RGBA")
            img3.save(filepath,dpi=(144,144))

            pixels = [32, 64, 256, 512, 1024]
            pixels2 = [16, 32, 128, 256, 512]
            filepaths = ['icon_16x16@2x.png','icon_32x32@2x.png','icon_128x128@2x.png','icon_256x256@2x.png','icon_512x512@2x.png']
            filepaths2 = ['icon_16x16.png','icon_32x32.png','icon_128x128.png','icon_256x256.png','icon_512x512.png']

            # dpi=144の各種pngファイル作成
            for pixel,file in zip(pixels,filepaths):
                img = Image.open(filepath)
                img_resize = img.resize((pixel,pixel))
                img_resize.save(filedir + file)
                img.close()

            # dpi=72の各種pngファイル作成    
            for pixel,file in zip(pixels2,filepaths2):
                img = Image.open(filepath2)
                img_resize = img.resize((pixel,pixel))
                img_resize.save(filedir + file)
                img.close()

            # icnsファイル作成
            dir = "/".join(filepath0.split("/")[:-1])
            iconset = (filepath0.split("/")[-1]).split(".")[-2] + ".iconset"
            cmd = f'iconutil -c icns {iconset}'
            subprocess.run(cmd, cwd=dir,shell=True)
            
            os.remove(filepath0_jpg)
            os.remove(filepath)
            os.remove(filepath2)
        else:
            MakeIcns.showDialog(self,window)
            
    def showDialog(self,window):
        dlg = QDialog(window)
        dlg.setFixedSize(250,100)
        label = QLabel('This file is invalid.\nPNG file[2048*2048,72dpi] required',dlg)
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        label.move(15,20)
        label.setStyleSheet('font-size:14px')
        btn = QPushButton("OK",dlg)
        btn.move(90,60)
        def action():
            dlg.close()
        btn.released.connect(action)
        dlg.setWindowTitle("Attention")
        dlg.exec()

[Python]328 PyQt6 UIデザインの実装

[M1 Mac, Big Sur 11.6.5, Python 3.10.0]

PyQt6ツールのUIデザインを実装しました。特に引っかかるところはなかったです。

あらかじめAdobeXDなどで設計しておくと座標やサイズはコピーするだけでいいので楽です。

import sys
from PyQt6.QtWidgets import QLabel,QWidget,QApplication,QTextEdit,QLineEdit,QPushButton,QButtonGroup,QRadioButton
from PyQt6.QtCore import Qt

class ImageInspector(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("IMAGE INSPECTOR")
        self.setGeometry(100,100,360,220)
        self.setStyleSheet('background-color: #708090')
        
        file = QLabel('File',self)
        file.setGeometry(15,15,26,16)
        file.setAlignment(Qt.AlignmentFlag.AlignCenter)
        
        input = QLineEdit('',self)
        input.setGeometry(50,10,220,25)
        
        execution = QPushButton('実行',self)
        execution.setGeometry(290,10,50,30)
        
        clear = QPushButton('クリア',self)
        clear.setGeometry(290,50,50,30)
        
        self.rbtns = QButtonGroup()
        inspect = QRadioButton("Inspect",self)
        inspect.setGeometry(50,40,90,20)
        self.rbtns.addButton(inspect)
        
        resize = QRadioButton("Resize",self)
        resize.setGeometry(50,65,90,20)
        self.rbtns.addButton(resize)
        
        width_label = QLabel('W',self)
        width_label.setGeometry(135,70,15,10)
        width_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        width_label.setStyleSheet('font-size:10px')
        
        width = QLineEdit('',self)
        width.setGeometry(155,65,45,20)
        
        height_label = QLabel('H',self)
        height_label.setGeometry(205,70,15,10)
        height_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        height_label.setStyleSheet('font-size:10px')
        
        height = QLineEdit('',self)
        height.setGeometry(220,65,45,20)
        
        icns = QRadioButton("icns作成",self)
        icns.setGeometry(50,90,90,20)
        self.rbtns.addButton(icns)
        
        output = QTextEdit('',self)
        output.setGeometry(50,115,220,100)

app = QApplication(sys.argv)
window = ImageInspector()
window.show()
app.exec()

[Python]327 appファイルのApple公証

[M1 Mac, Big Sur 11.6.5, Python 3.10.0]

py2appにより作成したappファイルをAppleに提出しApple公証をパスさせました。

appファイルはインタプリタやPyQtを丸ごとパッケージにしているので中にあるプログラムに署名する必要があります。今回は268個のファイルに署名しました。

署名対象ファイルはappファイル作成時にログ出力されるsignリストで確認できます。このリストを取り出して以下のコードで一括署名しました。

これでGUIのXCodeでは扱っていないJava, C++, PythonアプリのApple公証を成功させたことになります。

次の目標はC++とPythonアプリのMicrosoft Storeアプリ登録(コマンドでappxファイル作成)ですが、ハードルはかなり高そうです。特に多言語対応に不安があります。xmlが使えるのであれば問題ないのですが。

import subprocess

signs = [ログ出力から取得したファイルパス + Contents/MacOS内実行ファイル]

for sign in signs:
    cmd = f'codesign --force --verify --verbose --sign [mac-signing-key-user-name] {sign} --deep --options runtime --entitlements entitlements.plist --timestamp'
    subprocess.run(cmd,shell=True)

[C++] 08 FLTKアプリのApple公証

[M1 Mac, Big Sur 11.6.5, FLTK 1.3.8]

FLTKアプリのApple公証はJavaアプリとは異なりappファイルへの署名だけでパスしました。署名時の–options runtimeオプションは必須でした。

Javaアプリではappファイルの中にある実行ファイルや動的ライブラリにも署名が必要だったのでこれはありがたいです。

# 署名コマンド
codesign --force --verify --verbose \
    --sign [mac-signing-key-user-name] \
    "test.app" \
    --deep \
    --options runtime \
    --entitlements entitlements.plist \
    --timestamp

# 提出コマンド
xcrun altool --notarize-app -t osx -f "test.zip" \
    --primary-bundle-id [ID] \
    -u [登録メールアドレス] \
    -p [パスワード]

[Python]326 Pythonスクリプトのappファイル化

[M1 Mac, Big Sur 11.6.5, Python 3.10.0]

PythonスクリプトをappファイルにするMakefileを作成しました。あらかじめpy2appをpipでインストールしておきます。今回はPyQt6のGUIアプリで試してみました。

サイズがかなり大きくなるので実際に配布するのは厳しいでしょうか。Windowを表示させるだけのコードが200MBにもなりました。

こうなってくると最初からJavaのSwingなどで書く方がいいような気もします。

# コマンド設定
GENERATOR = py2applet
GENERATOR2 = python

# ファイル設定
SOURCE = test.py
SETUP = setup.py
TARGET = test.app

# ディレクトリ設定
BUILDDIR = build
DISTDIR = dist

# appファイル作成
$(TARGET):
	$(GENERATOR) --make-setup $(SOURCE)
	$(GENERATOR2) $(SETUP) py2app

# リセット&実行
all: clean $(TARGET)

# ファイル&ディレクトリ削除
clean:
	rm -rf $(BUILDDIR) $(DISTDIR) $(SETUP)

[Python]325 icnsファイルの作成 改良版

[M1 Mac, Big Sur 11.6.5]

2048*2048, dpi=72のpngファイルからicnsファイルを一発作成できるようにしました。手作業の時に比べたらかなりの省力化になります。

昨日の段階ではプログラムでpngの解像度を上げる方法がわかりませんでしたが、一旦jpgに変換するとdpiを72から144に上げることができました。

せっかくなのでTkinterでGUIアプリにしようとしましたが、相変わらずApple Siliconに対応できておらずButtonが真っ白のままです。PyQt5はHomebrewでインストールできたもののimportエラーになりました。PyQt5ディレクトリの中を確認するとほぼ空でした。Apple Siliconには非対応なのでインストールが中断していたのでしょう。

PyQt6をwhlファイルでインストールするとテストコードが正常に走りました。これでしばらく遊んでみます。なおGPLライセンス版なのでアプリ配布の際はコード開示請求に応える義務があります。

2022/03/25追記
pngからjpgに変換しなくても解像度を上げられましたので後日コードをアップします。

from PIL import Image
import subprocess

filepath0 = 'test0.png' # 2048*2048 dpi=72
filepath0_jpg = 'test.jpg' # 1024*1024 dpi=144
filepath = 'test.png' # 1024*1024 dpi=144
filepath2 = 'test2.png' # 512*512 dpi=72
filedir = '/test/test.iconset/'

img1 = Image.open(filepath0).convert("RGB")
# 512*512 dpi=72
img1_resize = img1.resize((512,512))
img1_resize.save(filepath2)

# dpi=144にするため一旦jpgへ変換
img1_resize2 = img1.resize((1024,1024))
img1_resize2.save(filepath0_jpg,dpi=(144,144))
# jpgをpngへ変換
img3 = Image.open(filepath0_jpg).convert("RGBA")
img3.save(filepath,dpi=(144,144))

pixels = [32, 64, 256, 512, 1024]
pixels2 = [16, 32, 64, 256, 512]
filepaths = ['icon_16x16@2x.png','icon_32x32@2x.png','icon_128x128@2x.png','icon_256x256@2x.png','icon_512x512@2x.png']
filepaths2 = ['icon_16x16.png','icon_32x32.png','icon_128x128.png','icon_256x256.png','icon_512x512.png']

# dpi=144の各種pngファイル作成
for pixel,file in zip(pixels,filepaths):
    img = Image.open(filepath)
    img_resize = img.resize((pixel,pixel))
    img_resize.save(filedir + file)
    img.close()

# dpi=72の各種pngファイル作成    
for pixel,file in zip(pixels2,filepaths2):
    img = Image.open(filepath2)
    img_resize = img.resize((pixel,pixel))
    img_resize.save(filedir + file)
    img.close()

# icnsファイル作成
cmd = 'iconutil -c icns test.iconset'
subprocess.run(cmd, cwd=r"/test",shell=True)
Button表示不良のため開発中断(Tkinter)