C++ アプリケーションでデザイナ UI ファイルを使用する

Qt Widgets Designer UI ファイルは、XML フォーマットでフォームのウィジェット・ツリーを表します。フォームは処理できます:

  • コンパイル時に、フォームはコンパイル可能な C++ コードに変換されます。
  • つまり、フォームがコンパイル可能な C++ コードに変換されます。ランタイム時とは、XML ファイルを解析しながらウィジェット ツリーを動的に構築するQUiLoader クラスによってフォームが処理されることを意味します。

コンパイル時のフォーム処理

Qt Widgets Designer でユーザーインターフェイスコンポーネントを作成し、Qt の統合ビルドツールであるqmakeuic を使用して、アプリケーションのビルド時にコンポーネントのコードを生成します。生成されたコードには、フォームのユーザー・インターフェース・オブジェクトが含まれています。これはC++の構造体で、以下を含みます:

  • フォームのウィジェット、レイアウト、レイアウト・アイテム、ボタン・グループ、アクションへのポインタ。
  • 親ウィジェットにウィジェット・ツリーを構築するsetupUi() というメンバ関数。
  • フォームの文字列プロパティの変換を処理するretranslateUi() というメンバ関数。詳細は、「言語の変更に対応する」を参照してください。

生成されたコードはアプリケーションに含めることができ、アプリケーションから直接使用できます。あるいは、標準ウィジェットのサブクラスを拡張するために使用することもできます。

コンパイル時に処理されたフォームは、次のいずれかのアプローチでアプリケーションで使用できます:

  • 直接アプローチ: コンポーネントのプレースホルダとして使用するウィジェットを作成し、その中にユーザー・インターフェースを設定します。
  • 単一継承アプローチ:フォームの基本クラス(QWidgetQDialog など)をサブクラス化し、フォームのユーザー・インターフェース・オブジェクトのプライベート・インスタンスを含める。
  • 多重継承: フォームの基底クラスとユーザー・インターフェース・オブジェクトの両方をサブクラス化します。これにより、フォームで定義されたウィジェットをサブクラスのスコープ内から直接使用できるようになります。

デモンストレーションのために、シンプルな電卓フォームアプリケーションを作成します。これはオリジナルのCalculator Form の例に基づいています。

このアプリケーションは、main.cpp と UI ファイルから構成されています。

Qt Widgets Designer でデザインしたcalculatorform.ui ファイルを以下に示します:

CMake を使って実行ファイルをビルドする場合は、CMakeLists.txt ファイルが必要です:

cmake_minimum_required(VERSION 3.16)
project(calculatorform LANGUAGES CXX)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)

find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)

qt_add_executable(calculatorform
                  calculatorform.ui main.cpp)

set_target_properties(calculatorform PROPERTIES
    WIN32_EXECUTABLE TRUE
    MACOSX_BUNDLE TRUE
)

target_link_libraries(calculatorform  PUBLIC
    Qt::Core
    Qt::Gui
    Qt::Widgets
)

このフォームは、qt_add_executable() の C++ ソース・ファイルの中にリストされています。CMAKE_AUTOUIC オプションは、uic ツールを実行して、ソース・ファイルで使用できるui_calculatorform.h ファイルを作成するよう、CMake に指示します。

qmake を使って実行ファイルをビルドする場合、.pro ファイルが必要である:

TEMPLATE    = app
FORMS       = calculatorform.ui
SOURCES     = main.cpp

このファイルの特長は、uic で処理するファイルをqmake に指示するFORMS 宣言です。この場合、calculatorform.ui ファイルは、SOURCES 宣言にリストされているどのファイルでも使用できるui_calculatorform.h ファイルを作成するために使用されます。

注意: Qt Creator を使って電卓フォームプロジェクトを作成することができます。Qt Creator は main.cpp、UI、ビルドツール用のプロジェクトファイルを自動的に生成します。

直接アプローチ

直接アプローチを使うには、ui_calculatorform.h ファイルをmain.cpp に直接インクルードします:

#include "ui_calculatorform.h"

main 関数は、calculatorform.ui ファイルによって記述されたユーザーインターフェイスをホストするために使用する標準QWidget を構築することによって、電卓ウィジェットを作成します。

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QWidget widget;
    Ui::CalculatorForm ui;
    ui.setupUi(&widget);

    widget.show();
    return app.exec();
}

この場合、Ui::CalculatorFormui_calculatorform.h ファイルのインターフェース記述オブジェクトで、ダイアログのすべてのウィジェットとそのシグナルとスロット間の接続を設定します。

直接的なアプローチは、シンプルで自己完結的なコンポーネントをアプリケーションで使用するための、迅速で簡単な方法を提供します。しかし、Qt Widgets Designerで作成されたコンポーネントは、アプリケーションの残りのコードと密接に統合する必要があります。例えば、CalculatorForm のコードはコンパイルされ実行されますが、QSpinBox オブジェクトはQLabel と相互作用しません。QLabel で追加操作を実行し、結果を表示するにはカスタムスロットが必要です。

単一継承アプローチ

単一継承を使用するには、標準のQtウィジェットをサブクラス化し、フォームのユーザー・インターフェース・オブジェクトのプライベート・インスタンスをインクルードします。これは以下のような形になります:

  • メンバ変数
  • ポインタ・メンバ変数

メンバ変数の使用

この方法では、Qtウィジェットをサブクラス化し、コンストラクタ内でユーザーインターフェースを設定します。この方法で使用されるコンポーネントは、フォームで使用されるウィジェットとレイアウトをQtウィジェットのサブクラスに公開し、ユーザーインターフェイスとアプリケーション内の他のオブジェクトとの間でシグナルとスロットの接続を行うための標準的なシステムを提供します。生成されたUi::CalculatorForm 構造体はクラスのメンバです。

このアプローチは電卓フォームの例で使われています。

ユーザー・インターフェースを確実に使用するためには、Ui::CalculatorForm を参照する前に、uic が生成するヘッダー・ファイルをインクルードする必要があります:

#include "ui_calculatorform.h"

プロジェクト・ファイルを更新してcalculatorform.h をインクルードする必要があります。CMake の場合:

qt_add_executable(calculatorform
    calculatorform.cpp calculatorform.h calculatorform.ui
    main.cpp
)

インクルードディレクティブが相対パスを使用している以下の例のような特定のケースでは、AUTOUICに依存する代わりにqt_add_uiを使用してui_calculatorform.h

AUTOUICよりもqt_add_uiを優先する場合

#include "src/files/ui_calculatorform.h"
qt_add_ui(calculatorform SOURCES calculatorform.ui
          INCLUDE_PREFIX src/files)

qmake の場合:

HEADERS     = calculatorform.h

サブクラスは次のように定義します:

class CalculatorForm : public QWidget
{
    Q_OBJECT

public:
    explicit CalculatorForm(QWidget *parent = nullptr);

private slots:
    void updateResult();

private:
    Ui::CalculatorForm ui;
};

このクラスの重要な特徴は、ユーザーインターフェースの設定と管理のためのコードを提供するプライベートなui オブジェクトです。

サブクラスのコンストラクタは、ui オブジェクトのsetupUi() 関数を呼び出すだけで、ダイアログのすべてのウィジェットとレイアウトを構築し、設定します。これが完了すると、必要に応じてユーザー・インターフェースを変更することができる。

CalculatorForm::CalculatorForm(QWidget *parent)
    : QWidget(parent)
{
    ui.setupUi(this);
    connect(ui.inputSpinBox1, &QSpinBox::valueChanged, this, &CalculatorForm::updateResult);
    connect(ui.inputSpinBox2, &QSpinBox::valueChanged, this, &CalculatorForm::updateResult);
}

ユーザ・インターフェイス・ウィジェットのシグナルとスロットは、on_<オブジェクト名> - 接頭辞を追加することで、通常の方法で接続できます。詳細はwidgets-and-dialogs-with-auto-connect を参照してください。

この方法の利点は、QWidget- ベースのインターフェイスを提供するために継承を簡単に使用できることと、ui データ・メンバ内にユーザー・インターフェイス・ウィジェット変数をカプセル化できることです。このメソッドを使用して、同じウィジェット内に複数のユーザー・インタフェースを定義し、それぞれを独自の名前空間内に格納して、それらをオーバーレイ(または合成)することができます。この方法は、例えば、既存のフォームから個々のタブを作成するために使用できます。

ポインタ・メンバ変数の使用

あるいは、Ui::CalculatorForm 構造体をクラスのポインタ・メンバにすることもできます。ヘッダーは次のようになります:

namespace Ui {
    class CalculatorForm;
}

class CalculatorForm : public QWidget
...
virtual ~CalculatorForm();
...
private:
    Ui::CalculatorForm *ui;
...

対応するソース・ファイルは次のようになります:

#include "ui_calculatorform.h"

CalculatorForm::CalculatorForm(QWidget *parent) :
    QWidget(parent), ui(new Ui::CalculatorForm)
{
    ui->setupUi(this);
}

CalculatorForm::~CalculatorForm()
{
    delete ui;
}

この方法の利点は、ユーザー・インターフェース・オブジェクトを前方宣言できることで、生成されたui_calculatorform.h ファイルをヘッダーに含める必要がありません。依存するソース・ファイルを再コンパイルすることなく、フォームを変更できる。これは、クラスがバイナリ互換性の制限を受ける場合に特に重要です。

一般的に、ライブラリや大規模なアプリケーションにはこの方法を推奨します。詳細は、共有ライブラリの作成 を参照してください。

多重継承のアプローチ

Qt Widgets Designer で作成したフォームは、標準のQWidget ベースのクラスと共にサブクラス化することができます。このアプローチにより、フォームで定義されたすべてのユーザー・インターフェース・コンポーネントにサブクラスのスコープ内で直接アクセスできるようになり、connect() 関数を使用して通常の方法でシグナルとスロットの接続を行うことができます。

次のように、uiccalculatorform.ui ファイルから生成するヘッダー・ファイルをインクルードする必要があります:

#include "ui_calculatorform.h"

このクラスは、単一継承の場合と同じように定義されるが、今回はQWidgetUi::CalculatorForm両方を継承する:

class CalculatorForm : public QWidget, private Ui::CalculatorForm
{
    Q_OBJECT

public:
    explicit CalculatorForm(QWidget *parent = nullptr);

private slots:
    void on_inputSpinBox1_valueChanged(int value);
    void on_inputSpinBox2_valueChanged(int value);
};

Ui::CalculatorForm を私的に継承して、ユーザー・インターフェース・オブジェクトがサブクラスで私的になるようにしています。また、ui をpublicまたはprotectedにしたのと同じように、public またはprotected キーワードで継承することもできます。

サブクラスのコンストラクタは、単一継承の例で使用したコンストラクタと同じタスクを実行します:

CalculatorForm::CalculatorForm(QWidget *parent)
    : QWidget(parent)
{
    setupUi(this);
}

この場合、ユーザー・インターフェースで使用されるウィジェットは、コードで作成されたウィジェットと同じようにアクセスできます。ui この場合、ユーザーインターフェイスで使用されるウィジェットは、コードで作成されたウィジェットと同じようにアクセスすることができます。

言語変更への対応

Qt は、ユーザーインターフェースの言語が変更されると、QEvent::LanguageChange タイプのイベントを送信してアプリケーションに通知します。ユーザーインターフェースオブジェクトのメンバ関数retranslateUi() を呼び出すには、次のようにフォームクラスでQWidget::changeEvent() を再実装します:

void CalculatorForm::changeEvent(QEvent *e)
{
    QWidget::changeEvent(e);
    switch (e->type()) {
    case QEvent::LanguageChange:
        ui->retranslateUi(this);
        break;
    default:
        break;
   }
}

実行時のフォーム処理

別の方法として、フォームを実行時に処理し、動的に生成されたユーザー・インターフェースを生成することもできます。これは、Qt Widgets Designerで作成されたフォームを処理するためのQUiLoader クラスを提供するQtUiTools モジュールを使用して行うことができます。

UiToolsのアプローチ

実行時にフォームを処理するには、UIファイルを含むリソース・ファイルが必要です。また、QtUiTools モジュールを使用するようにアプリケーションを設定する必要があります。これは、CMake プロジェクト・ファイルに以下の宣言を記述し、アプリケーションのコンパイルとリンクが適切に行われるようにします。

find_package(Qt6 REQUIRED COMPONENTS Core Gui UiTools Widgets)
target_link_libraries(textfinder PUBLIC
    Qt::Core
    Qt::Gui
    Qt::UiTools
    Qt::Widgets
)

qmake の場合:

QT += uitools

QUiLoader クラスは、ユーザー・インターフェースを構築するためのフォーム・ ローダー・オブジェクトを提供します。このユーザー・インターフェースは、QIODevice 、例えばQFile オブジェクトから取得することができ、プロジェクトのリソース・ファイルに格納されているフォームを取得することができます。QUiLoader::load ()関数は、ファイルに含まれるユーザー・インターフェース記述を使用してフォーム・ウィジェットを構築します。

QtUiTools モジュール・クラスは、以下のディレクティブを使ってインクルードできます:

#include <QtUiTools>

QUiLoader::load() 関数は、テキスト・ファインダーの例のコードに示されているように呼び出されます:

static QWidget *loadUiFile(QWidget *parent)
{
    QFile file(u":/forms/textfinder.ui"_s);
    file.open(QIODevice::ReadOnly);

    QUiLoader loader;
    return loader.load(&file, parent);
}

実行時にQtUiTools を使用してユーザー・インターフェースを構築するクラスでは、QObject::findChild() を使用してフォーム内のオブジェクトを見つけることができます。例えば、次のコードでは、オブジェクト名とウィジェット・タイプに基づいてコンポーネントを検索しています:

    ui_findButton = findChild<QPushButton*>("findButton");
    ui_textEdit = findChild<QTextEdit*>("textEdit");
    ui_lineEdit = findChild<QLineEdit*>("lineEdit");

実行時にフォームを処理することで、開発者はUIファイルを変更するだけで、プログラムのユーザー・インターフェースを自由に変更できる。これは、さまざまなユーザーのニーズに合わせてプログラムをカスタマイズするときに便利です。たとえば、特別に大きなアイコンを表示したり、アクセシビリティをサポートするために異なる配色にしたりすることができます。

自動接続

コンパイル時またはランタイムフォーム用に定義されたシグナルとスロットの接続は、手動で設定することも、QMetaObject の機能を使用して、シグナルと適切な名前のスロット間の接続を自動的に行うこともできます。

一般的に、QDialog 、ユーザーが入力した情報を受理する前に処理したい場合、OKボタンのclicked()シグナルをダイアログのカスタムスロットに接続する必要があります。まず、手作業でスロットを接続するダイアログの例を示し、次に自動接続を使用するダイアログと比較します。

自動接続を使用しないダイアログ

前と同じようにダイアログを定義しますが、コンストラクタに加えてスロットを定義します:

class ImageDialog : public QDialog, private Ui::ImageDialog
{
    Q_OBJECT

public:
    explicit ImageDialog(QWidget *parent = nullptr);

private slots:
    void checkValues();
};

checkValues() スロットは、ユーザーによって提供された値を検証するために使用されます。

ダイアログのコンストラクタでは、前と同じようにウィジェットを設定し、キャンセルボタンのclicked() シグナルをダイアログの reject() スロットに接続します。また、両方のボタンのautoDefault プロパティを無効にして、ダイアログがライン編集がリターンキーイベントを処理する方法を妨げないようにします:

ImageDialog::ImageDialog(QWidget *parent)
    : QDialog(parent)
{
    setupUi(this);
    okButton->setAutoDefault(false);
    cancelButton->setAutoDefault(false);
    ...
    connect(okButton, &QAbstractButton::clicked, this, &ImageDialog::checkValues);
}

OKボタンのclicked ()シグナルをダイアログのcheckValues()スロットに接続します:

void ImageDialog::checkValues()
{
    if (nameLineEdit->text().isEmpty()) {
        QMessageBox::information(this, tr("No Image Name"),
            tr("Please supply a name for the image."), QMessageBox::Cancel);
    } else {
        accept();
    }
}

このカスタムスロットは、ユーザーによって入力されたデータが有効であることを確認するために必要な最小限のことだけを行います。

自動接続のウィジェットとダイアログ

ダイアログにカスタムスロットを実装し、コンストラクタで接続するのは簡単ですが、代わりにQMetaObject の自動接続機能を使用して、OKボタンの clicked() シグナルをサブクラスのスロットに接続することができます。uic は、ダイアログのsetupUi() 関数でこれを行うコードを自動的に生成するので、標準的な規約に従った名前のスロットを宣言して実装するだけです:

void on_<object name>_<signal name>(<signal parameters>);

注意: フォーム内のウィジェットの名前を変更する場合、スロット名もそれに合わせて変更する必要があります。このため、新しいコードではこの方法を使わないことをお勧めします。

この規約を使用すると、OKボタンのマウスクリックに応答するスロットを定義して実装できます:

class ImageDialog : public QDialog, private Ui::ImageDialog
{
    Q_OBJECT

public:
    explicit ImageDialog(QWidget *parent = nullptr);

private slots:
    void on_okButton_clicked();
};

信号とスロットの自動接続のもう1つの例は、on_findButton_clicked() スロットを持つテキストファインダーでしょう。

QMetaObject のシステムを使い、シグナルとスロットの接続を可能にする:

    QMetaObject::connectSlotsByName(this);

これにより、以下のようにスロットを実装することができます:

void TextFinder::on_findButton_clicked()
{
    QString searchString = ui_lineEdit->text();
    QTextDocument *document = ui_textEdit->document();

    bool found = false;

    // undo previous change (if any)
    document->undo();

    if (searchString.isEmpty()) {
        QMessageBox::information(this, tr("Empty Search Field"),
                                 tr("The search field is empty. "
                                    "Please enter a word and click Find."));
    } else {
        QTextCursor highlightCursor(document);
        QTextCursor cursor(document);

        cursor.beginEditBlock();
    ...
        cursor.endEditBlock();

        if (found == false) {
            QMessageBox::information(this, tr("Word Not Found"),
                                     tr("Sorry, the word cannot be found."));
        }
    }
}

シグナルとスロットの自動接続は、標準的な命名規則と、ウィジェット設計者が作業するための明示的なインターフェースの両方を提供します。与えられたインターフェイスを実装するソースコードを提供することで、ユーザー・インターフェイス設計者は、自分自身でコードを書くことなく、設計が実際に動作することを確認することができます。

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。