[C++] 13 Qt5 GUIアプリ作成 QtWidgets

[M1 Mac, Big Sur 11.6.5]

GUI画面が出来上がりました。基本的にはPyQt6版のコードをコピペしてメソッドのピリオドを->に書き換えただけです。オブジェクト作成の際にnewを付けたり、行の最後にセミコロンを入れるのを忘れがちでした。

次はボタン動作ですが、Qtではシグナル/スロットという仕組みになっています。ざっと説明を読んだものの今ひとつピンとこないです。

#include <QtWidgets/QApplication>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QButtonGroup>
#include <QtWidgets/QRadioButton>
#include <QtWidgets/QTextEdit>
#include <QtWidgets/QWidget>
#include <QMainWindow>

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

    QMainWindow* mainWin = new QMainWindow();
    mainWin->setGeometry(100,100,360,220);
    mainWin->setWindowTitle("IMAGE INSPECTOR");
    mainWin->setStyleSheet("background: '#708090';");

    QLabel* file = new QLabel(mainWin);
    file->setText("File");
    file->setGeometry(15,15,35,16);
    file->setStyleSheet("foreground: '#FFFAFA';font-size:12px;");

    QLineEdit* input= new QLineEdit(mainWin);
    input->setGeometry(50,10,220,25);
    
    QPushButton* execution = new QPushButton(mainWin);
    execution->setText("実行");
    execution->setGeometry(290,10,50,30);
    execution->setStyleSheet("foreground: '#FFFAFA';font-size:12px;");
    QPushButton::connect(execution, SIGNAL( clicked() ),&app, SLOT(quit()) );

    QPushButton* clear = new QPushButton(mainWin);
    clear->setText("クリア");
    clear->setGeometry(290,50,50,30);
    clear->setStyleSheet("foreground: '#FFFAFA';font-size:12px;");

    QButtonGroup* rbtns = new QButtonGroup(mainWin);
    QRadioButton* inspect = new QRadioButton(mainWin);
    inspect->setText("Inspect");
    inspect->setGeometry(50,40,90,20);
    inspect->setChecked(true);
    rbtns->addButton(inspect);
    
    QRadioButton* resize_img = new QRadioButton(mainWin);
    resize_img->setText("Resize");
    resize_img->setGeometry(50,65,90,20);
    rbtns->addButton(resize_img);

    QLabel* width_label = new QLabel(mainWin);
    width_label->setText("W");
    width_label->setGeometry(135,70,15,10);
    width_label->setStyleSheet("font-size:10px;");
    
    QLineEdit* width= new QLineEdit(mainWin);
    width->setGeometry(155,65,45,20);
    
    QLabel* height_label = new QLabel(mainWin);
    height_label->setText("H");
    height_label->setGeometry(205,70,15,10);
    height_label->setStyleSheet("font-size:10px;");
        
    QLineEdit* height= new QLineEdit(mainWin);
    height->setGeometry(220,65,45,20);
        
    QRadioButton* icns = new QRadioButton(mainWin);
    icns->setText("icns作成");
    icns->setGeometry(50,90,90,20);
    icns->setToolTip("PNG file[2048*2048,72px] required");
    rbtns->addButton(icns);
    
    QTextEdit* output = new QTextEdit(mainWin);
    output->setGeometry(50,115,240,100);
    
    mainWin->show();
    return app.exec();
}

[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>

[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 [パスワード]

[C++] 07 FLTKのMakefile作成 appファイル作成箇所変更

[M1 Mac, Big Sur 11.6.4, FLTK 1.3.8]

Info.plistにできるだけ手を入れないよう、binディレクトリの実行ファイルをMakefileがあるsrcディレクトリにコピーしてからappファイルを作成しました。

実行ファイルをコピーして最後に消去するという工程が増えるのでコードの行数自体はさほど変わらないですが、plistファイルの中はあまりいじらない方を選択しました。今回はiconファイルの設定だけに留めています。ゆくゆくはバージョン番号やコピーライトを追記します。

Makefileの隣にappファイルが作成されるため、動作確認もすぐにでき効率的になりました。

<Makefileの一部>

# oファイルから実行ファイルとappファイル作成
$(TARGET): $(OBJECTS)
	$(COMPILER) -o $(TARGETDIR)/$@ $(OBJECTS) $(LINK) $(LDFLAGS)
	cp $(TARGETDIR)/$(TARGET) $(TARGET)
	$(POSTBUILD) $(TARGET)
	mkdir $(TARGET).app/Contents/Resources
	cp ../images/ColorSampleJP.icns $(TARGET).app/Contents/Resources
	plutil -insert 'CFBundleIconFile' -string "test.icns" $(TARGET).app/Contents/Info.plist
	rm -f $(TARGET)

[C++] 06 ウィジェットツールキットの比較

C++で使える主なウィジェットツールキットを比較しました。

wxWidgetsもお手頃ですが、OSのLook & Feelを採用しているのが私にはネックです。個人的にWindowsのL&Fが好みではありません。ファイルサイズが大きいというのもマイナスです。

当初の予定通り、FLTKで進めていくことにしました。

[C++] 05 FLTKのMakefile作成 Info.plistの修正

[M1 Mac, Big Sur 11.6.4, FLTK 1.3.8]

Makefileを更新しました。

“fltk-config –post”コマンドではMakefileと実行ファイルが同じディレクトリにないとappファイル内に作成されるInfo.plistの内容がおかしくなります。相対パスがMakefile基準なので修正する必要があります。

そのため一旦作成されたInfo.plistを修正するコマンドを追加しました。iconファイルを設定するキーも追加しています。

# コンパイラ設定他
COMPILER = clang++
DEBUG = -g

# フラグ設定
CXXFLAGS = $(shell fltk-config --use-gl --use-images --cxxflags ) -std=c++11
LDFLAGS = $(shell fltk-config --use-gl --use-images --ldflags ) -lc++ 

# includeパス設定
INCLUDE = -I../include -I/opt/homebrew/Cellar/fltk/1.3.8/include

# linkパス設定
LINK = -L/opt/homebrew/Cellar/jpeg/9e/lib -L/opt/homebrew/Cellar/libpng/1.6.37/lib

# 実行ファイル設定
TARGET = test
TARGETDIR = ../bin

# ソースコードパス
SRCROOT   = .

# oファイルの出力ディレクトリ
OBJROOT   = ../obj

# ソースディレクトリのリスト化
SRCDIRS := $(shell find $(SRCROOT) -type d)

# ソースディレクトリから全てのcppファイルをリスト化
SOURCES   = $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.cpp))

# cppファイルのリストからオブジェクトファイル名を設定
OBJECTS   = $(addprefix $(OBJROOT), $(patsubst ./%,/%,$(SOURCES:.cpp=.o)))

# oファイルの出力ディレクトリをリスト化
OBJDIRS   = $(addprefix $(OBJROOT), $(patsubst ./%,/%,$(SRCDIRS)))

# 依存dファイルをoファイルから作成
DEPENDS   = $(OBJECTS:.o=.d)

# 依存ファイル
-include $(DEPENDS)

# cppファイルからoファイル作成
$(OBJROOT)/%.o: $(SRCROOT)/%.cpp
	@if [ ! -e `dirname $@` ]; then mkdir -p `dirname $@`; fi
	$(COMPILER) $(CXXFLAGS) $(INCLUDE) $(DEBUG) -o $@ -c $<

# アプリファイル作成関連
POSTBUILD  = fltk-config --post

# oファイルから実行ファイルとappファイル作成
$(TARGET): $(OBJECTS)
	$(COMPILER) -o $(TARGETDIR)/$@ $(OBJECTS) $(LINK) $(LDFLAGS)
	$(POSTBUILD) $(TARGETDIR)/$@
	mkdir $(TARGETDIR)/$(TARGET).app/Contents/Resources
	cp ../images/test.icns $(TARGETDIR)/$(TARGET).app/Contents/Resources
	plutil -replace 'CFBundleExecutable' -string "test" $(TARGETDIR)/$(TARGET).app/Contents/Info.plist
	plutil -insert 'CFBundleIconFile' -string "test.icns" $(TARGETDIR)/$(TARGET).app/Contents/Info.plist
	plutil -replace 'CFBundleIdentifier' -string "org.fltk.test" $(TARGETDIR)/$(TARGET).app/Contents/Info.plist

[C++] 04 FLTK:Fl_Tabs, Fl_Group, Fl_Button

[M1 Mac, Big Sur 11.6.1, FLTK 1.3.8]

自製GUIアプリの移植に本格着手しました。まずはFL_Buttonの格子状配置です。

もっと苦労するかと思いましたが、案外すんなりでした。Java・SwingのGridBagLayoutを使うより断然書きやすいです。ただ日本語ユーザーでFLTKを使っている方はネットではほとんど見かけず、少ない英語情報を参考に基本的には自分でマニュアルを読みながら進めていくという形になります。

FL_Buttonのconst char*型引数を作成する所で配列の要素をそのまま渡すと配列の最後の文字列が全てのボタンに表示されてしまうというトラブルがありましたが、要素のポインタを渡すと各々の文字列が問題なく表示されました。ポインタはしばらく扱っていなかったので慣れるまで少し時間がかかりそうです。

移植開始前、C/C++とJavaは似ても似つかぬ言語だという印象でした。書き進めていくとFor文の書き方や配列からの値の取り出し方などC/C++がJavaにかなりの影響を与えているところが垣間見え、徐々に親しみが湧いてきました。

ネットにてC言語の様々な自作関数を目にしますが、実はC++では正式な関数として用意されているといったケースが多々あり、余計な遠回りをしないよう注意を払う必要があります。

今回は危うくsubstr関数を自製しようとしましたし、16進数の文字列を数値に変換する際、C++11から採用のstoiを使わずにatoiで処理しかけました。C言語専門あるいはC++11以降を知らない方も多くいらっしゃるようです。

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Tabs.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Widget.H>

using std::cout; using std::cin;
using std::endl; using std::string;
using std::vector; using std::stoi;

int main(int argc, char **argv) {
    Fl_Window *window = new Fl_Window(50,50,660,480);

    <色名などの配列は省略>

    Fl_Tabs *tabs = new Fl_Tabs(10,10,400,480);
    {
        for (auto &i : tab_names) {
            string tab_name = i;
            const char* tab_name_p = i.c_str();
            Fl_Group *grp = new Fl_Group(30,30,380,440,tab_name_p);{
                int num = 0;
                grp->labelsize(10);
                for (int x = 0; x < 5; x++){
                    int loc_x = x * 75 + 23;
                    for (int y = 0; y < 28; y++){
                        int loc_y = y * 15 + 43;

                        string* color_name_p = &(color_names[num]);
                        const char* color_name_p2 = color_name_p->c_str();

                        string color_code = color_codes[num];
                        string red0 = color_code.substr(2,2);
                        int red = stoi(red0, nullptr, 16);
                        
                        string green0 = color_code.substr(4,2);
                        int green = stoi(green0, nullptr, 16);

                        string blue0 = color_code.substr(6,2);
                        int blue = stoi(blue0, nullptr, 16);

                        Fl_Button *button = new Fl_Button(loc_x, loc_y, 75, 15,color_name_p2);
                        button->color(fl_rgb_color(red,green,blue));
                        button->labelcolor(fl_rgb_color(169,169,169));
                        button->labelsize(8);
                        num = num + 1;
                    }
                } 
            }
            grp->end();
        }
    }
    tabs->end();
    
    window->end();
    window->show(argc, argv);
    return Fl::run();
}