ユーザ用ツール

ハイハイスクールアドベンチャー Qt版

あらすじ

2019年神奈山県立ハイ高等学校は 地盤が弱く校舎の老朽化も進んだため、 とうとう廃校にする以外方法がなく なってしまった。

ところで大変な情報を手に入れた。 それは、

「ハイ高校にATOMIC BOMBが仕掛けられている。」

と、いうものだ。 どうやらハイ高が廃校になった時、 気が狂った理科の先生がATOMIC BOMBを 学校のどこかに仕掛けてしまったらしい。

お願いだ。我が母校のコナゴナになった 姿を見たくはない。 早くATOMIC BOMBを取り除いてくれ……!!

行動は英語で、“<動詞>” 或いは、“<動詞>”+“<目的語>“のように入れていただきたい。 例えば、”look room”と入れれば部屋の様子を見ることが出来るという訳だ。

それでは Good Luck!!!…………

概要

M5版を作った勢いで、SDL2版を作ろうと思ったのだが、Textureの扱いがイマイチ判然とせず、動き始めたものの表示が不規則に切り替わり、それを直すくらいならと、Qtにスイッチしたのがこのバージョンである。

入出力部以外はM5版と同じである。

ダウンロードとインストール

いくつかの環境用にビルドしたパッケージを用意してあるので、それを使ってインストールすればいろいろ面倒なことなく遊べます。 下にある説明に従ってビルドしてもいいかと思います。

Windows版はインストーラを走らせるだけです。

Linux版はdeb形式のパッケージになっています。 今どきのデスクトップ環境だと、メニューにも登録されるかと思います。

ご自分の環境にあったパッケージを取得したら、

$ sudo apt install -y ./qhhsadv_1.0.0_ubuntu24.04.02_amd64.deb

などとしてインストールしてください。

ビルド

QtCreatorを使用して作成したので、QtCreatorを使ってビルドするのが簡単だが、cmakeで直接ビルドすることも可能である。 ソースはM5版同様GitHubから取得可能である。 データファイルは実行時に自動的に ~/.local/share/WildTreeJP/QHHSAdv の下に展開される。

GitHubからは git clone で取得する。

$ git clone https://github.com/wildtree/qhhsadv.git

QtCreatorを使用する場合には、CMakeLists.txt がプロジェクトファイルになる。 cmakeでビルドするなら、

$ mkdir build
$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX=/ ..
$ make
$ sudo make install

と、通常の cmakeの作法に従えばいい。

実行は qhhsadv を実行する。

インストールは /opt/qhhsadv に実行ファイル、リソースなどをインストールし、/usr/bin/qhhsadv に起動用のラッパーを、また、デスクトップ環境用のメニューへの追加用の設定ファイルもインストールされる、

ハイハイスクールアドベンチャー

あれこれ

ダイアログについて

M5版とは異なり、自由度の高いダイアログなどを用意できる関係で、「ストーリー」や「このプログラムについて」などのダイアログを実装したが、スタッフクレジットが意外と長くて、環境によっては画面に収まらないことが判明。

だったら、スクロールするようにすればいいじゃんと思ったのだが、これが以外にもめんどくさかった。

スクロールバーつきのダイアログ

Qtではこの種のダイアログは QMessageBox::about() を使って表示するのだが、残念なことにスクロールバーがつくことはない。 なので、自力でそのようなダイアログをつくらなければならない。

普通に考えれば、QDialogのインスタンスを作って、そこに必要な部品をはめていけばいいのだが、うっかり、QMessageBoxクラスを継承して、うまいことごにょごにょすればいいんじゃないかと思ってしまった。

それが、間違いだったが、始めてしまったからには仕方がない。

方針としては、QMessageBoxで表示してる QLabelをスクロールバーつきにしてやればいいのだ。 そうだ、そんなに難しいことではない……と、思ってた。

ウィジェットをいくつも詰め込む場合にはレイアウトも必要になるが、今回は QLabelひとつを QScrollAreaに突っ込むだけなので大したことはない。

QScrollArea *scrollArea = new QScrollArea();
QLabel *label = new QLabel(text);
scrollArea->setWidget(label);

これを、元々表示している QLabelと入れ替えてやればいい。 QMessageBoxには QGridLayout がはりついていて、そこにQLabel がはまっているので、そのGridLayoutに対して差し替えをさせればいい。

QGridLayout *l = qobject_cast<QGridLayout*>(this->layout());
l->addWidget(scrollArea, 0, 2, 1, -1); // アプリケーションアイコンがある場合。ない場合は 2->1に置き換える 

ところが、これやったら、ダイアログの左上に出てくるだけで、狙ったところにスクロールバーはつかない。 1)

もうなんか、アイコンのあるなしで場所代わるし、レイアウト触るんじゃなくて、表示してる QLabel の中身をスクロールバーつきの QLabelに変えちゃえばいいんじゃね? ということに気づいた。 勿論、正しいやり方かどうかは知らない。 そこまでQtに詳しいわけじゃない。

    QLabel *label = this->findChild<QLabel*>("qt_msgbox_label");
    QLabel *content = new QLabel(label->text());
    content->setWordWrap(true);
    content->setOpenExternalLinks(true);
    label->clear();
    _scrollArea = new QScrollArea(label);
    _scrollArea->setWidget(content);
    _scrollArea->setWidgetResizable(true);
    _scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    _scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    _scrollArea->setMinimumSize(label->width(),label->height());
    label->setMinimumSize(label->width(),label->height());

最後、最小サイズを指定してやらないとダイアログが極小になってしまうのでこれがある意味キモ。

なお、QMessageBoxを継承した場合は void about()も定義しなおしておかないといけない。 最後の setMinimunSize()に関しては、StyleSheetでやっている例もあったが、QLabel { min-width: 300 px; min-height: 300 px; }みたいにしてしまうと、textだけでなく、アイコンの方もそのサイズにされてしまうので、アイコン付きの about()では都合が悪いのだ。

void
ScrollMessageBox::about(QWidget *parent, const QString &title, const QString &text)
{
    ScrollMessageBox *scrBox = new ScrollMessageBox(QMessageBox::Icon::Information, title, text, QMessageBox::StandardButton::Ok, parent);
    scrBox->setAttribute(Qt::WA_DeleteOnClose);
    QIcon icon = scrBox->windowIcon();
    QSize size = icon.actualSize(QSize(64, 64));
    scrBox->setIconPixmap(icon.pixmap(size));

    scrBox->exec();
}

Qt::WA_DeleteOnClose をセットしておくことで、ダイアログを閉じたら勝手に削除してくれるので staticな関数なのに、オブジェクトを返さずにすむ。 お手軽だが、こんなの、Qtのソースみないとわからない。

Qt5/Qt6の互換性

主にuConsoleでコード書いていた関係で、Qt5を使って開発したため、Qt6で動くかどうかとか全然ちゃんと調べてなかった。 DevTermの方は逆にQt6をインストールしてあったので、そっちでビルドできるかトライしたらあっさりこけた。

ただし、こけたのは QMediaPlayer 周り。

音鳴らす機能はおまけなのでなくてもいい。

とりあえず、動作チェックのために、Qt6 なら飛ばすようにした。

#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
        _mp.setMedia(QUrl::fromLocalFile(files()->mp3_file(sound_names[n]).c_str()));
        _mp.setVolume(100);
        _mp.play();
#endif

ざっくり、Qt6.0.0より前のバージョンの時だけコンパイルされるようにした。

でも待って。Qt6のリファレンスにサンプルコードあるじゃん。 ってことで、速攻で Qt6用のコードも追加した。 音もちゃんと鳴った。

#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
        _mp.setMedia(QUrl::fromLocalFile(files()->mp3_file(sound_names[n]).c_str()));
        _mp.setVolume(100);
        _mp.play();
#else
        _mp.setAudioOutput(&_audio);
        _mp.setSource(QUrl::fromLocalFile(files()->mp3_file(sound_names[n]).c_str()));
        _audio.setVolume(100);
        _mp.play();
#endif

ボリュームの制御が QAudioOutput に移されてて、あとは setMedia()が setSource()になっただけ。 とはいえ、違いは違い。

テーマ対応

ハイハイスクールアドベンチャー Android版ハイハイスクールアドベンチャー Windows版と、テーマ対応を成し遂げてきたのであれば、Qt版でも対応するのが筋というものでしょう。

Qtのテーマ対応は、ちょっと見ただけだとそれほど難しくも手間でもなさそうです。 但し、完璧を期さないのであれば、ですが。

テーマ対応の概要

いくつかやり方はあるようですが、テーマファイル2)で作って、それを適用するのが柔軟性がありそうでよさそうです。

しかも、Windows版と違って、書くこと少なくていいです。

light.qss
/* light qss */
QWidget {
        background-color: #ffffff;
        color: #000000;
}
 
QPushButton {
        background-color: #e0e0e0;
        border: 1px solid #888;
        padding: 4px 8px;
}
dark.qss
/* dark qss */
QWidget {
        background-color: #2b2b2b;
        color: #dddddd;
}
 
QPushButton {
        background-color: #444444;
        border: 1px solid #666;
        padding: 4px 8px;
}
void
applyTheme(bool dark)
{
    QString fileName = QString(theme_file(dark).c_str());
    QFile f(fileName);
    if (f.open(QFile::ReadOnly))
    {
        QString style = QLatin1String(f.readAll());
        qApp->setStyleSheet(style);
    }
}

やっぱりタイトルバー

Qtもテーマ対応をしたところで、タイトルバーは「対象外」なんだとのことです。 もちろん、Windows版同様、カスタムタイトルバーを使うことで対応できるようです。 そして、Windows版同様、ボタンの実装や処理もやらないといけないようです。

ここで問題となるのが、マルチプラットフォーム対応の場合。 要するに、どこまでやれるのかがまだよくわかってない3)ので、一応この部分はまだ保留にしてあります。

システムテーマの検出

Qt6.5.0以降は機種非依存でシステムテーマを取り出すことができる。 なので、この機能を使えば、機種ごとに頑張ってテーマの検出コードを書く必要がない。

頑張って機種ごとのテーマ検出コード書くくらいなら、世の中のQtが全部6.5以降になるのを期待して、Qt6.5以降のみ、「システム追随」が機能するってことで良しとしています。

        bool dark = false;
        if (theme == ThemeType::System)
        {
#if QT_VERSION >= QT_VERSION_CHECK(6,5,0)
            dark = qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark;
#endif
        }
        else
        {
            dark = (theme == ThemeType::Dark);
        }
        this->applyTheme(dark);

QSSの限界

WindowsのXAMLでもHTMLにおけるCSSでも、コーディングなしでスライドスイッチ風ウィジェットを実現できます。 QtでもQSSでできるのかなあと思ってAIに聞いたら「できます」というので、やってみたら、全然できませんでした。

最終的に、AIも「それはQSSの限界ですね」といって、あっさり、カスタムウィジェット+カスタムスタイルで逃げちゃいました。

QSSはCSSのサブセットみたいなものですが、本当に装飾的な部分しか扱えないようです。

チェックボックスをスライドスイッチに

チェックボックスでも機能的には十分なのですが、今どき、スライドスイッチ風があるべき姿でしょう。 QSSでできると期待したのにできなかった奴です。

なお、現状のバージョンはスライドスイッチをアニメーションで切り替えたりはできません。 ザクっとON/OFFが切り替わるやつです。

スイッチ部品をテーマに対応させるために、プロパティだけを提供するためのラッパークラスを用意します。

switchbox.h
#ifndef SWITCHBOX_H
#define SWITCHBOX_H
 
#include <QObject>
#include <QCheckBox>
 
class SwitchBox : public QCheckBox {
    Q_OBJECT
    Q_PROPERTY(QColor switchColorOn READ switchColorOn WRITE setSwitchColorOn)
    Q_PROPERTY(QColor switchColorOff READ switchColorOff WRITE setSwitchColorOff)
    Q_PROPERTY(QColor switchButtonColor READ switchButtonColor WRITE setSwitchButtonColor)
 
public:
    explicit SwitchBox(QWidget *parent = nullptr)
        : QCheckBox(parent),
        m_on(Qt::blue),
        m_off(Qt::gray),
        m_button(Qt::white) {}
 
    QColor switchColorOn() const { return m_on; }
    void setSwitchColorOn(const QColor &c) { m_on = c; }
 
    QColor switchColorOff() const { return m_off; }
    void setSwitchColorOff(const QColor &c) { m_off = c; }
 
    QColor switchButtonColor() const { return m_button; }
    void setSwitchButtonColor(const QColor &c) { m_button = c; }
 
private:
    QColor m_on;
    QColor m_off;
    QColor m_button;
};
 
#endif // SWITCHBOX_H

これだけでは足りないので、スイッチボックス風のスタイルを提供するクラスを作ります。

switchstyle.h
#ifndef SWITCHSTYLE_H
#define SWITCHSTYLE_H
 
#include <QProxyStyle>
#include <QStyleOption>
#include <QStyleOptionButton>
#include <QPainter>
#include <QRect>
#include <QColor>
 
class SwitchStyle : public QProxyStyle {
public:
    using QProxyStyle::QProxyStyle;
 
    // QSS から色を取得(プロパティベース)
    QColor getSwitchColor(const QWidget *widget, bool checked) const {
        const char *propName = checked ? "switchColorOn" : "switchColorOff";
        QVariant colorProp = widget ? widget->property(propName) : QVariant();
        if (colorProp.canConvert<QColor>())
            return colorProp.value<QColor>();
        return checked ? QColor("#1E90FF") : QColor("#AAAAAA"); // デフォルト
    }
 
    QColor getButtonColor(const QWidget *widget) const {
        QVariant colorProp = widget ? widget->property("switchButtonColor") : QVariant();
        if (colorProp.canConvert<QColor>())
            return colorProp.value<QColor>();
        return Qt::white; // デフォルト
    }
 
    QRect subElementRect(SubElement element,
                         const QStyleOption *option,
                         const QWidget *widget) const override {
        if (element == SE_CheckBoxIndicator) {
            // チェックボックス全体の矩形
            QRect fullRect = option->rect;
 
            // インジケータのサイズ(例:40x20)
            int w = 40;
            int h = 20;
 
            // fullRect の右端にインジケータを配置
            int x = fullRect.right() - w;
            int y = fullRect.top() + (fullRect.height() - h) / 2;
 
            return QRect(x, y, w, h);
        }
        return QProxyStyle::subElementRect(element, option, widget);
    }
 
    QSize sizeFromContents(ContentsType type,
                           const QStyleOption *option,
                           const QSize &contentsSize,
                           const QWidget *widget) const override {
        QSize sz = QProxyStyle::sizeFromContents(type, option, contentsSize, widget);
        if (type == CT_CheckBox)
        {
            sz.setHeight(std::max(sz.height(), 24));
            sz.setWidth(std::max(sz.width(), 80)); // ← 幅を確保
        }
        return sz;
    }
 
    int pixelMetric(PixelMetric metric,
                    const QStyleOption *option,
                    const QWidget *widget) const override {
        if (metric == PM_IndicatorWidth) return 40;
        if (metric == PM_IndicatorHeight) return 20;
        return QProxyStyle::pixelMetric(metric, option, widget);
    }
 
    void drawPrimitive(PrimitiveElement element,
                       const QStyleOption *option,
                       QPainter *painter,
                       const QWidget *widget = nullptr) const override {
        if (element == PE_IndicatorCheckBox) {
            if (!option) return;
            QRect r = option->rect;
            painter->setRenderHint(QPainter::Antialiasing);
 
            bool checked = option->state & State_On;
            QColor trackColor = getSwitchColor(widget, checked);
 
            // 背景(トラック)
            painter->setBrush(trackColor);
            painter->setPen(Qt::NoPen);
            painter->drawRoundedRect(r, r.height()/2, r.height()/2);
 
            // つまみ(白い円)
            int knobDiameter = r.height() - 4;
            int x = checked ? r.right() - knobDiameter - 2 : r.left() + 2;
            QRect knobRect(x, r.top() + 2, knobDiameter, knobDiameter);
 
            painter->setBrush(getButtonColor(widget));
            painter->drawEllipse(knobRect);
            return;
        }
        QProxyStyle::drawPrimitive(element, option, painter, widget);
    }
};
 
#endif // SWITCHSTYLE_H

最初は、描画するdrawPrimitive()だけだったのですが、QSSから色情報をとりたいとか、スイッチをダイアログの右端に配置しようとしたら、右にはみ出したり、ラベルの高さより高くなってしたが削られたりしたのでsizeFromContents()を投入したり、右にはみ出さなくなったけれど、今度は、右端の部分しか表示されないのでsubElementRect()で十分な幅が得られるようにしたりと、この分量になりました。

このスイッチは、色について三つのプロパティを持っています。

  • qproperty-switchColorOn Onの時の背景色
  • qproperty-switchColorOff Offの時の背景色
  • qproperty-switchButtonColor ボタン色

QSSにはこんな感じで設定します。

SwitchBox {
    qproperty-switchColorOn: #4CC2FF;
    qproperty-switchColorOff: #CCCCCC;
    qproperty-switchButtonColor: #FFFFFF;
}

使い方はこんな感じで。

    SwitchBox *chkSound = new SwitchBox();
    chkSound->setStyle(new SwitchStyle(chkSound->style()));
    chkSound->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    chkSound->setChecked(_sound);
    QObject::connect(chkSound, SIGNAL(stateChanged(int)), this, SLOT(setSound(int)));
    QGridLayout *grid0 = new QGridLayout;
    grid0->addWidget(new QLabel("音を鳴らす"), 0, 0, Qt::AlignLeft);
    grid0->addWidget(chkSound, 0, 1, Qt::AlignRight);

SwitchBox のインスタンスを作ったら、setStyleで SwitchStyleのインスタンスを割り当てます。 ここまでしてやるとスライドスイッチ風のウィジェットが出来上がります。 中身は見ての通り CheckBoxそのものなんですけれどね。

環境非依存化

Qtはマルチプラットフォームのツールキットなので、適切にコーディングすれば、Linuxだけでなく、WindowsやMacOSでも動作させることができます。

当初、Qt版はLinux上で開発していたため、GUIまわり以外の部分は、UNIX由来のサービスを使いまくっていたので、Windows版Qtでビルドしようとしたら、山ほどエラーが出まして、これを機に環境依存コードを排除して、全面的に書き直しました。

ファイル入出力も、Qtのコールに書き換えました。 これは、WindowsとUNIX(とMacOS)で改行コードの扱いが違うことで、ファイル操作の際に余計な処理が入る関係です。

とりあえず、WindowsとLinux 4)でのビルドが可能なことは確認しました。 5)

Windows版

Windows版を実行したら、コンソールウィンドウが開く、コンソールアプリでした。 調べたところ、エントリーポイントが main()だとコンソールアプリ、WinMain()だと非コンソールアプリになるとか。

え、てことは #if とかでエントリポイントを変える感じ?

と、思ったら他にもやり方がありました。 CMakeLists.txt でコントロールできるそうで。 qt_add_executable()に WIN32 ってつければいいんだとか。 結局 if(WIN32)とかで切り替えるわけですが6)、プログラム側でやるよりはすっきりして異様に思います。

if(WIN32)
    set(app_icon_resource_windows "${CMAKE_CURRENT_SOURCE_DIR}/qhhsadv.rc")
    list(APPEND PROJECT_SOURCES "${app_icon_resources_windows}")
endif()
 
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
    if(WIN32)
        qt_add_executable(qhhsadv WIN32
            ${PROJECT_SOURCES}
            qhhsadv.rc
        )
    else()
        qt_add_executable(qhhsadv
            ${PROJECT_SOURCES}
        )
    endif()
 
    qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
else()
    if(ANDROID)
        add_library(qhhsadv SHARED
            ${PROJECT_SOURCES}
        )
    else()
        add_executable(qhhsadv
            ${PROJECT_SOURCES}
        )
    endif()
 
    qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
endif()

リソースフォルダーのコピー

リソースデータは AppLocalDataLocation/WildTreeJP/QHHSAdv の下にあることを想定しています。 なければ、プログラムのあるディレクトリにある data ディレクトリの中身をコピーしてから動作を開始します。

なので、ソースツリーの中で管理されているリソースディレクトリを、ビルド時に、ビルドディレクトリにコピーしてほしいのです。

CMakeLists.txt を利用する場合は次のようになります。

add_custom_command(TARGET qhhsadv POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
            ${CMAKE_SOURCE_DIR}/data
            $<TARGET_FILE_DIR:qhhsadv>/data
)

インストーラとパッケージ

Qtはマルチプラットフォームなので、ビルド後の扱いをまじめに考えるとちょっとめんどくさい。

基本的に、GitHubから持って行って、自分でビルドして遊んで、でもいいんですが、Linuxはともかく、WindowsにいちいちQtをインストールしてっていうのはなかなかにハードルが高く、遊んでもらえません。

Linuxにしても、今どきのユーザはパッケージをインストールして使うスタイルの人も多いのでしょう。 何しろ、環境をセットアップして最初にやるべきことが build-essentialsをインストールすることなのですから。

なので、インストールとパッケージについてあれこれ書いておきます。

Linux

Linuxの場合は、ビルドしてインストールすることから始めます。 パッケージも、インストールされたイメージをもとに作るので、まずはインストールなのです。

make を走らせるのだから、make install すればいいだろうということですが、そのためには installターゲットが生成されなければなりません。

Makefileは cmakeが作るのだから、cmakeに作るように指示しなければなりません。

if(UNIX AND NOT APPLE)
    # 実行ファイルを /opt/qhhsadv に配置
    install(TARGETS qhhsadv DESTINATION /opt/qhhsadv)
 
    # data ディレクトリを /opt/qhhsadv にコピー
    install(DIRECTORY ${CMAKE_BINARY_DIR}/data DESTINATION /opt/qhhsadv)
 
    # ラッパースクリプトを /usr/bin にインストール
    install(PROGRAMS ${CMAKE_SOURCE_DIR}/scripts/qhhsadv.sh
            DESTINATION /usr/bin
            RENAME qhhsadv)
    # .desktop ファイル
    install(FILES ${CMAKE_SOURCE_DIR}/resources/linux/qhhsadv.desktop
            DESTINATION /usr/share/applications)
 
    # アイコン(48x48 の例)
    install(FILES ${CMAKE_SOURCE_DIR}/resources/icons/qhhsadv.png
            DESTINATION /usr/share/icons/hicolor/48x48/apps)
endif()

ビルドはマルチターゲットなので、Linuxは UNIXかつ非APPLEという扱いになります。 ここに、インストールするものとその行先をずらずらと書いていきます。

DESTINATIONに行き先を指定するわけですが、PREFIXとかつけないでいいのか? といわれれば、それはcmakeがよしなにやってくれるから、気にせず、/から書いておけばいいのです。

データリソースの扱いは、本来、UNIX系の慣習に倣うなら、/usr/share/qhhsadv の下に置くべきなのでしょうが、Windowsっぽく、プログラムバイナリのある場所にある想定なので、今更、ソースから書き換えるのもめんどくさいです。

なので、こういう感じでインストールする先として、最近では /opt が使われるので、/opt/qhhsadv の下に、プログラムもリソースも一緒くたに置くことにしています。

/usr/bin には /opt/qhhsadv/qhhsadv を呼び出すだけのスクリプトを置いておきます。

あとは、

$ cmake -DCMAKE_INSTALL_PREFIX=/ ..
$ make
$ sudo make install

とすれば、インストールしてくれます。

パッケージング

以前なら、Slackware系、RedHat系、Debian系がしのぎを削っていましたが、今どきはもうDebian系一択でいいでしょう。 deb形式のパッケージにまとめることを想定しています。

<del>パッケージングするためには、仮想ルートフォルダの下にイメージをインストールします。

$ make install DESTDIR=$PWD/package-root

これで、./package-root ができて、その下にインストールされた形になります。

次に、パッケージ用のファイルを作ります。

$ mkdir ./package-root/DEBIAN

./package-root/DEBAIN というフォルダを作ったら、その下に control という名前のファイルを作ります。 これは Ubuntu24.04.02 用のファイルになります。

control
Package: qhhsadv
Version: 1.0.0
Section: games
Priority: optional
Architecture: amd64
Maintainer: Hiroyuki Araki <wildtree@gmail.com>
Depends: libc6 (>= 2.31), libqt6core6t64 (>= 6.4), libqt6multimedia6 (>=6.4), libqt6widgets6t64 (>= 6.4)
Description: High High School Adventure for Qt
 A Qt-based High High School Adventure implementation.

あとは、パッケージの作成を行います。

$ dpkg-deb --root-owner-group --build package-root qhhsadv_1.0.0_ubutu24.04.2_amd64.deb

</del>

パッケージングでcontrolファイルを毎回作るの面倒くさいとAIにもちかけたら、CPackにすればいいじゃん、といわれました。 なにそれ?

if(UNIX AND NOT APPLE)
    # パッケージの基本情報
    set(CPACK_PACKAGE_NAME ${APP_NAME})
    set(CPACK_PACKAGE_VERSION ${APP_VERSION})
    set(CPACK_PACKAGE_CONTACT "wildtree@gmail.com")
 
    # Debian パッケージ用のメタ情報
    set(CPACK_GENERATOR "DEB")
    set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Hiroyuki Araki") # 必須
    set(CPACK_DEBIAN_PACKAGE_SECTION "games")
    set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6core6 (>= 6.4.0), libc6 (>= 2.34), libqt6widget6 (>= 6.4.0)")
 
    # アーキテクチャ指定(自動判定も可能)
    string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" DETECTED_ARCH)
 
    if(DETECTED_ARCH STREQUAL "x86_64")
        set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
    elseif(DETECTED_ARCH STREQUAL "aarch64")
        set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64")
    elseif(DETECTED_ARCH MATCHES "armv7")
        set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "armhf")
    else()
        set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "${DETECTED_ARCH}") # fallback
    endif()
 
    include(CPack)
endif()

インストールターゲットとか特には必要としません。 パッケージの作成までは以下の手順になります。

$ git clone https://github.com/wildtree/qhhsadv.git
$ cd qhhsadv
$ cmake -B build -DCMAKE_BUILD_TYPE=Release
$ cmake --build build --target all
$ cpack -G DEB -C Release --config build/CPackConfig.cmake

インストールは dpkgでも aptでも好きなのでインストーラを走らせるだけです。 カレントフォルダに qhhsadv_1.1.1_Linux.deb のようなファイルができます。 バージョン番号は、CMakeLists.txtの頭の方に書いてあります。

# Application basic information
set(APP_NAME "qhhsadv")
set(APP_VERSION_MAJOR "1")
set(APP_VERSION_MINOR "1")
set(APP_VERSION_PATCH "1")
set(APP_VERSION "${APP_VERSION_MAJOR}.${APP_VERSION_MINOR}.${APP_VERSION_PATCH}")
$ sudo apt install -y ./qhhsadv_1.0.0_ubuntu24.04.02_amd64.deb

Windows

Windowsの場合は、Linuxのように、直接 make でインストールするのは現実的ではありません。 なので、インストーラを作ってそれでインストールする形をとります。

WiXかInno Setup 6のどっちかが定番らしいですが、WiXは基本的に Visal Studio 2022のプラグインとして使うのが普通なので、Qt Creator や cmakeでやるのはちょっとめんどくさそうです。

なので、Inno Setup 6を使うことにします。 Inno Setup 6は winget でインストールできます。

PS C:\> winget install JRSoftware.InnoSetup

DEBIANのcontrol同様、Inno Setup には Setup.issという設定ファイルを作る必要があります。

setup.iss.in
; ハイハイスクールアドベンチャー インストーラ (Inno Setup)
 
[Setup]
AppName=High High School Adventure Qt
AppVersion=@APP_VERSION@
AppPublisher=WildTreeJP
DefaultDirName={commonpf}\QHHSAdv
DefaultGroupName=ハイハイスクールアドベンチャー
UninstallDisplayIcon={app}\qhhsadv.exe
OutputBaseFilename=qhhsadv_setup
Compression=lzma
SolidCompression=yes
 
[Languages]
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
Name: "english"; MessagesFile: "compiler:Default.isl"
 
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
 
[Files]
; Qtアプリ本体と依存DLLをまとめたフォルダを指定
Source: "deploy\qhhsadv.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "deploy\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion

; リソースディレクトリ
Source: "deploy\data\*"; DestDir: "{app}\data"; Flags: recursesubdirs createallsubdirs ignoreversion
 
[Icons]
Name: "{group}\ハイハイスクールアドベンチャー"; Filename: "{app}\qhhsadv.exe"
Name: "{commondesktop}\ハイハイスクールアドベンチャー"; Filename: "{app}\qhhsadv.exe"; Tasks: desktopicon
 
[Run]
; インストール完了後に起動するオプション
Filename: "{app}\qhhsadv.exe"; Description: "{cm:LaunchProgram,ハイハイスクールアドベンチャー}"; Flags: nowait postinstall skipifsilent

デプロイするのに必要なファイルやフォルダを、インストーラの作業ディレクトリの下の deploy にまとめることにします。 この方針に合うように CMakeLists.txt に WIN32用の記述を追加していきます。

if(WIN32)
    # デプロイ先ディレクトリ
    set(DEPLOY_DIR ${CMAKE_BINARY_DIR}/installer/deploy)
 
    # コピー対象
    set(APP_EXE $<TARGET_FILE:qhhsadv>)
    set(DATA_DIR ${CMAKE_BINARY_DIR}/data)
 
    # デプロイ処理
    add_custom_command(
        TARGET qhhsadv POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E make_directory ${DEPLOY_DIR}
        COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:qhhsadv> ${DEPLOY_DIR}
        COMMAND ${CMAKE_COMMAND} -E copy_directory ${DATA_DIR} ${DEPLOY_DIR}/data
        COMMENT "Copying files to installer/deploy..."
    )
 
    # Qt DLL やプラグインを集める場合(windeployqt)
    # Qt bin ディレクトリを探す
    get_target_property(QT_QMAKE_EXECUTABLE Qt6::qmake IMPORTED_LOCATION)
    get_filename_component(QT_BIN_DIR "${QT_QMAKE_EXECUTABLE}" DIRECTORY)
 
    set(WINDEPLOYQT_EXECUTABLE "${QT_BIN_DIR}/windeployqt.exe")
 
    add_custom_command(
        TARGET qhhsadv POST_BUILD
        COMMAND "${WINDEPLOYQT_EXECUTABLE}" --dir ${DEPLOY_DIR} $<TARGET_FILE:qhhsadv>
        COMMENT "Running windeployqt..."
    )
 
    add_custom_command(
        TARGET qhhsadv POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/installer
        COMMENT "Copying setup.iss..."
    )
    configure_file(${CMAKE_SOURCE_DIR}/installer/setup.iss.in ${CMAKE_BINARY_DIR}/installer/setup.iss @ONLY)
 
    # Inno Setup コンパイラを探す
 
    find_program(INNO_SETUP_COMPILER
        NAMES ISCC iscc
        HINTS
            "$ENV{LOCALAPPDATA}/Programs/Inno Setup 6"
    )
    # PATH に入っている場合も探す
    if(NOT INNO_SETUP_COMPILER)
        find_program(INNO_SETUP_COMPILER NAMES ISCC iscc)
    endif()
 
    if(INNO_SETUP_COMPILER)
        message(STATUS "Inno Setup found: ${INNO_SETUP_COMPILER}")
 
        # .iss スクリプトの場所
        set(INNO_SCRIPT ${CMAKE_BINARY_DIR}/installer/setup.iss)
 
        # 出力先(例: build ディレクトリ内)
        set(INSTALLER_OUTPUT ${CMAKE_BINARY_DIR}/installer/qhhsadv_setup.exe)
 
        add_custom_command(
            OUTPUT ${INSTALLER_OUTPUT}
            COMMAND "${INNO_SETUP_COMPILER}" "${INNO_SCRIPT}"
            DEPENDS qhhsadv  # アプリ本体がビルドされてから実行
            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
            COMMENT "Building Inno Setup installer..."
        )
 
        add_custom_target(installer ALL
            DEPENDS ${INSTALLER_OUTPUT}
        )
    else()
        message(WARNING "Inno Setup (ISCC.exe) not found. Skipping installer target.")
    endif()
endif()

Windows版Qtには、必要なDLLなどをまとめてくれる windeployqt.exe というツールが入っていて、これがDLLをかき集めてくれます。 ランタイムパッケージなどの提供がないQtなので、これはWindows版をデプロイするのには必須です。

生成されたプログラム、リソースなどをコピーして、DLLなどをかき集めたら、ISCC.exe を呼び出して、インストーラに仕上げます。

ここまでを、Windows環境でビルドを走らせると一気にやってくれます。

1)
後で考えたらもしかすると QGridLayout l = qobject_cast<QGridLayout>(this→widget()→layout() ); とするべきだったのかもしれない。
2)
QSS: Qt Style Sheet
3)
調査中
4)
Ubuntu 24.04LTS, Ubuntu 25.04、Debian 12(ARM64)で確認
5)
なお、Apple系のデバイスは持ってないし買う気もないのでできるかどうかはどなたか試してください。
6)
WIN32は関係ない環境では無視されるらしいので、別にifいらんのですが、アイコンの処理とかの関連もあるので。

This website uses cookies. By using the website, you agree with storing cookies on your computer. Also, you acknowledge that you have read and understand our Privacy Policy. If you do not agree, please leave the website.

More information