Qt モーダル処理の代替案:より良いUIのためのヒント集

2025-05-27

Qtプログラミングにおける QWidget::modal は、あるウィンドウ(ウィジェット)がモーダル状態になると、そのモーダルウィンドウが閉じられるまで、アプリケーションの他のすべてのウィンドウへのユーザーインタラクションをブロックするという性質を表します。

より具体的に説明すると、

  • モーダルウィンドウは、通常、何らかの処理を完了させる(例えば、設定ダイアログで設定を完了する、警告メッセージで「OK」をクリックするなど)か、キャンセルされるまで表示され続けます。
  • 他のアプリケーションのウィンドウ は通常通り操作できますが、同じアプリケーション内の他のウィンドウ は操作できなくなります。これには、親ウィンドウや他の兄弟ウィンドウも含まれます。
  • モーダルウィンドウが表示されている間、ユーザーはモーダルウィンドウ内のコントロール(ボタン、テキスト入力など)としかやり取りできません。

QWidget::modal の状態は、QWidgetクラスの setModal(bool) 関数を使って設定できます。true を渡すとモーダルになり、false を渡すと非モーダル(デフォルト)になります。また、isModal() 関数で現在のモーダル状態を確認できます。

モーダルウィンドウは、ユーザーに特定のタスクに集中させる必要がある場合に便利です。例えば、

  • 重要な警告やエラーメッセージ
    ユーザーにメッセージを認識させ、何らかのアクション(「OK」をクリックするなど)を取らせる必要があります。
  • 設定ダイアログ
    ユーザーは設定を変更して適用するか、キャンセルするまで、アプリケーションの他の部分を操作できません。
  • ファイルを開く/保存ダイアログ
    ユーザーはファイルを選択するか、ファイル名を指定するまで、メインウィンドウの操作に戻れません。

モーダルウィンドウには、大きく分けて以下の2つの種類があります。

  • ウィンドウモーダル (Window Modal)
    Qt::WindowModal フラグを指定して表示されたウィンドウは、そのウィンドウの親ウィンドウ、およびその親ウィンドウに関連する他のウィンドウのみをブロックします。
  • アプリケーションモーダル (Application Modal)
    Qt::ApplicationModal フラグを指定して表示されたウィンドウは、アプリケーション全体の他のすべてのウィンドウをブロックします。これが最も一般的なモーダルウィンドウです。

一般的には、QDialog クラスを継承したクラスを作成し、そのダイアログをモーダルとして表示することが多いです。QDialog クラスは、exec() 関数を提供しており、これを使うとダイアログがモーダルに表示され、ユーザーがダイアログを閉じるまで処理が一時停止します。



  1. モーダルウィンドウが閉じられない

    • 原因
      モーダルウィンドウを表示した後に、適切なタイミングで close() 関数や accept() (QDialogの場合)、reject() (QDialogの場合) 関数が呼び出されていない可能性があります。
    • 解決策
      モーダルウィンドウ内で、ユーザーの操作(ボタンクリックなど)に応じて、ウィンドウを閉じる処理を実装する必要があります。例えば、OKボタンがクリックされたら accept() を、キャンセルボタンがクリックされたら reject() を呼び出すようにします。

    • // モーダルダイアログのOKボタンのスロット
      void MyDialog::onOkButtonClicked() {
          accept(); // ダイアログを閉じ、QDialog::Accepted の結果を返す
      }
      
      // モーダルダイアログのキャンセルボタンのスロット
      void MyDialog::onCancelButtonClicked() {
          reject(); // ダイアログを閉じ、QDialog::Rejected の結果を返す
      }
      
  2. アプリケーションがフリーズする (応答なしになる)

    • 原因
      • モーダルウィンドウを表示したスレッド(通常はメインGUIスレッド)で、時間のかかる処理を実行している可能性があります。モーダルウィンドウが表示されている間、GUIスレッドはイベントループを処理していますが、重い処理でブロックされるとフリーズしたように見えます。
      • モーダルウィンドウを表示した後に、イベントループが適切に処理されていない可能性があります。
    • 解決策
      • 時間のかかる処理は、別のスレッドに移動して実行し、処理結果をシグナルとスロットの仕組みを使ってGUIスレッドに通知するようにします。
      • モーダルウィンドウを表示する前に、イベントループが正常に動作していることを確認します。通常、exec() (QDialogの場合) を使用してモーダル表示する場合は、Qtが自動的にイベントループを管理します。show() で表示し、setModal(true) を設定する場合は、明示的にイベントループを回す必要はありませんが、他の処理との兼ね合いで問題が発生することがあります。
    • 注意
      while(!done) { QCoreApplication::processEvents(); } のようなbusy-waitingは、CPUリソースを無駄にし、アプリケーションの応答性を悪化させるため避けるべきです。
  3. 意図しないウィンドウがブロックされる

    • 原因
      モーダルウィンドウの種類(Application Modal vs. Window Modal)を誤って選択している可能性があります。Qt::ApplicationModal を使用すると、アプリケーション全体の他のウィンドウがブロックされます。特定の親ウィンドウに関連するウィンドウのみをブロックしたい場合は、Qt::WindowModal を使用するべきです。
    • 解決策
      モーダルウィンドウの目的と、ブロックしたい範囲に応じて、適切なモーダルタイプを選択します。setWindowModality() 関数で設定できます。

    • QDialog *dialog = new QDialog(this); // 'this' が親ウィンドウ
      dialog->setWindowModality(Qt::WindowModal); // 親ウィンドウとその関連ウィンドウのみをブロック
      dialog->show();
      
  4. モーダルウィンドウの親が正しく設定されていない

    • 原因
      モーダルウィンドウの親ウィンドウが適切に設定されていない場合、ウィンドウマネージャーによっては期待通りに動作しないことがあります。特に、ウィンドウの位置や前面への表示などで問題が発生することがあります。
    • 解決策
      モーダルウィンドウを作成する際に、適切な親ウィンドウをコンストラクタに渡すようにします。

    • MyDialog *modalDialog = new MyDialog(this); // 'this' が親ウィンドウ
      modalDialog->exec(); // モーダル表示
      
  5. モーダルダイアログの結果を受け取れない

    • 原因
      QDialog::exec() を使用してモーダルダイアログを表示した場合、ダイアログが閉じられた後にその結果(QDialog::Accepted または QDialog::Rejected など)を受け取る必要があります。この結果を適切に処理していないと、ユーザーの選択に応じた処理が行えません。
    • 解決策
      exec() 関数の戻り値を確認し、それに応じて処理を行います。

    • MyDialog dialog(this);
      if (dialog.exec() == QDialog::Accepted) {
          // OKボタンがクリックされた場合の処理
          qDebug() << "OKが選択されました。";
          // ...
      } else {
          // キャンセルボタンがクリックされたか、ダイアログが閉じられた場合の処理
          qDebug() << "キャンセルされました。";
          // ...
      }
      
  6. 複数のモーダルウィンドウを同時に表示しようとする

    • 原因
      一般的に、複数のアプリケーションモーダルウィンドウを同時に表示することは推奨されません。ユーザーインターフェースが混乱し、操作が困難になる可能性があります。
    • 解決策
      モーダルウィンドウは、一つのタスクを完了させるために使用し、同時に複数のモーダルウィンドウを表示することは避けるべきです。もし複数の情報を提示する必要がある場合は、タブ付きのダイアログや、非モーダルな複数のウィンドウの使用を検討します。
  7. モーダルウィンドウが常に最前面に表示されない

    • 原因
      ウィンドウマネージャーの設定や、他のアプリケーションのウィンドウの特性によって、モーダルウィンドウが常に最前面に表示されないことがあります。Qtは通常、モーダルウィンドウを最前面に表示するように試みますが、OSやウィンドウマネージャーの制約を受けることがあります。
    • 解決策
      Qtの setWindowFlag() 関数で Qt::WindowStaysOnTopHint フラグを設定することで、常に最前面に表示するようにヒントを与えることができますが、OSやユーザーの設定によっては効果がない場合もあります。モーダルウィンドウの性質上、通常は最前面に表示されるべきですが、どうしても問題が解決しない場合は、OSやウィンドウマネージャー固有の動作を確認する必要があるかもしれません。


基本的なモーダルダイアログの例 (QDialogを使用)

最も一般的なモーダルウィンドウの作成方法は、QDialog クラスを継承したカスタムダイアログを作成し、exec() 関数を使ってモーダル表示することです。

#include <QApplication>
#include <QDialog>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QDebug>

class MyModalDialog : public QDialog {
public:
    MyModalDialog(QWidget *parent = nullptr) : QDialog(parent) {
        setWindowTitle("モーダルダイアログ");
        setModal(true); // 明示的にモーダルに設定することも可能ですが、exec()を使う場合は通常不要

        QVBoxLayout *layout = new QVBoxLayout(this);
        QLabel *label = new QLabel("このダイアログはモーダルです。");
        QPushButton *okButton = new QPushButton("OK");
        QPushButton *cancelButton = new QPushButton("キャンセル");

        layout->addWidget(label);
        layout->addWidget(okButton);
        layout->addWidget(cancelButton);

        connect(okButton, &QPushButton::clicked, this, &QDialog::accept); // OKボタンがクリックされたら accept() を呼び出す
        connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); // キャンセルボタンがクリックされたら reject() を呼び出す

        setLayout(layout);
    }
};

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

    QWidget mainWindow;
    QPushButton *openDialogButton = new QPushButton("モーダルダイアログを開く", &mainWindow);

    QObject::connect(openDialogButton, &QPushButton::clicked, [&]() {
        MyModalDialog dialog(&mainWindow); // 親ウィンドウを指定
        int result = dialog.exec(); // モーダル表示し、結果(AcceptedまたはRejected)を受け取る

        if (result == QDialog::Accepted) {
            qDebug() << "モーダルダイアログの結果: OK";
            // OKが選択された場合の処理
        } else {
            qDebug() << "モーダルダイアログの結果: キャンセル";
            // キャンセルが選択された場合の処理
        }
    });

    QVBoxLayout *mainLayout = new QVBoxLayout(&mainWindow);
    mainLayout->addWidget(openDialogButton);
    mainWindow.setLayout(mainLayout);
    mainWindow.show();

    return a.exec();
}

コードの説明

  1. MyModalDialog クラス
    QDialog を継承したカスタムダイアログクラスです。

    • コンストラクタで、ウィンドウのタイトルを設定し、ラベルとOKボタン、キャンセルボタンを作成しています。
    • QVBoxLayout を使用して、これらのウィジェットを縦に配置しています。
    • connect() 関数を使って、OKボタンの clicked シグナルが QDialog::accept() スロットに、キャンセルボタンの clicked シグナルが QDialog::reject() スロットに接続されています。これらのスロットは、ダイアログを閉じ、それぞれ QDialog::Accepted または QDialog::Rejected の結果を返します。
    • setModal(true) を明示的に呼び出すこともできますが、exec() を使用する場合は通常、ダイアログは自動的にモーダルになります。
  2. main 関数

    • メインウィンドウ (mainWindow) を作成し、その上に「モーダルダイアログを開く」というボタン (openDialogButton) を配置しています。
    • ボタンの clicked シグナルにラムダ関数を接続しています。このラムダ関数内で、MyModalDialog のインスタンス (dialog) を作成し、親ウィンドウとして mainWindow を渡しています。
    • dialog.exec() を呼び出すことで、ダイアログがモーダルに表示されます。この関数は、ユーザーがダイアログを閉じるまで処理をブロックし、ダイアログの結果(QDialog::Accepted または QDialog::Rejected)を返します。
    • 戻り値 (result) をチェックし、それに応じてメッセージをデバッグ出力しています。

QWidgetの setModal(true) を直接使用する例 (あまり一般的ではない)

QDialog を使用せずに、通常の QWidget をモーダルにする方法もありますが、QDialog の方がより高レベルで便利なので、一般的ではありません。

#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QDebug>

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

    QWidget mainWindow;
    QPushButton *openModalWidgetButton = new QPushButton("モーダルウィジェットを開く", &mainWindow);

    QObject::connect(openModalWidgetButton, &QPushButton::clicked, [&]() {
        QWidget *modalWidget = new QWidget(&mainWindow);
        modalWidget->setWindowTitle("モーダルウィジェット");
        modalWidget->setModal(true); // QWidgetをモーダルに設定

        QVBoxLayout *layout = new QVBoxLayout(modalWidget);
        QLabel *label = new QLabel("これはモーダルなQWidgetです。");
        QPushButton *closeButton = new QPushButton("閉じる");

        layout->addWidget(label);
        layout->addWidget(closeButton);
        modalWidget->setLayout(layout);

        QObject::connect(closeButton, &QPushButton::clicked, modalWidget, &QWidget::close);

        modalWidget->show(); // show() で表示。exec() は QDialog 特有の関数
    });

    QVBoxLayout *mainLayout = new QVBoxLayout(&mainWindow);
    mainLayout->addWidget(openModalWidgetButton);
    mainWindow.setLayout(mainLayout);
    mainWindow.show();

    return a.exec();
}

コードの説明

  1. ここでは、MyModalDialog のような専用のダイアログクラスは作成せず、直接 QWidget のインスタンス (modalWidget) を作成しています。
  2. modalWidget->setModal(true); を呼び出すことで、この QWidget がモーダルになります。
  3. show() 関数を使ってモーダルウィジェットを表示します。QDialog::exec() とは異なり、QWidget::show() はすぐに制御を返します。そのため、この方法ではダイアログが閉じられた後の結果を直接取得することはできません。
  4. 閉じるボタン (closeButton) の clicked シグナルを modalWidgetclose() スロットに接続することで、ボタンがクリックされたときにウィジェットが閉じられるようにしています。

注意点

  • QDialog は、モーダルダイアログとしてよく使われる共通の機能(結果コードの管理など)をあらかじめ持っているため、モーダルなウィンドウを作成する場合は QDialog を継承する方法が推奨されます。
  • QWidget::setModal(true) を使用してモーダルにした場合、ダイアログが閉じられた後の結果を直接取得する仕組みは QDialog::exec() ほど明確ではありません。通常は、ダイアログ内のウィジェットの状態をシグナルやパブリックメンバを通じて取得する必要があります。


  1. 非モーダルダイアログ (Non-Modal Dialogs)

    • 説明
      QDialogQWidgetsetModal(false) (デフォルト) の状態で show() 関数を使って表示します。非モーダルダイアログは、表示されていても親ウィンドウを含む他のウィンドウと自由にインタラクションできます。
    • 利点
      ユーザーはダイアログを開いたまま、他の作業を並行して行えます。
    • 欠点
      モーダルウィンドウのように、特定のタスク完了までユーザーの操作を強制できません。データの整合性や処理の順序が重要な場合には注意が必要です。
    • 使用例
      設定ウィンドウ、ツールパレット、プロパティインスペクタなど。
    • コード例
      #include <QApplication>
      #include <QDialog>
      #include <QVBoxLayout>
      #include <QPushButton>
      #include <QLabel>
      
      class MyNonModalDialog : public QDialog {
      public:
          MyNonModalDialog(QWidget *parent = nullptr) : QDialog(parent) {
              setWindowTitle("非モーダルダイアログ");
              QVBoxLayout *layout = new QVBoxLayout(this);
              QLabel *label = new QLabel("このダイアログは非モーダルです。");
              QPushButton *closeButton = new QPushButton("閉じる");
              layout->addWidget(label);
              layout->addWidget(closeButton);
              connect(closeButton, &QPushButton::clicked, this, &QDialog::close);
              setLayout(layout);
          }
      };
      
      int main(int argc, char *argv[]) {
          QApplication a(argc, argv);
          QWidget mainWindow;
          QPushButton *openDialogButton = new QPushButton("非モーダルダイアログを開く", &mainWindow);
          MyNonModalDialog *dialog = nullptr; // ダイアログのインスタンスを保持
      
          QObject::connect(openDialogButton, &QPushButton::clicked, [&]() {
              if (!dialog) {
                  dialog = new MyNonModalDialog(&mainWindow);
                  dialog->setAttribute(Qt::WA_DeleteOnClose); // ウィンドウが閉じられたら自動的に削除
              }
              dialog->show();
          });
      
          QVBoxLayout *mainLayout = new QVBoxLayout(&mainWindow);
          mainLayout->addWidget(openDialogButton);
          mainWindow.setLayout(mainLayout);
          mainWindow.show();
          return a.exec();
      }
      
  2. ウィジェット内のインタラクション (In-Widget Interaction)

    • 説明
      ダイアログを表示する代わりに、親ウィンドウ内のウィジェットの状態を変化させることで、ユーザーからの入力を受け付けたり、フィードバックを提供したりします。例えば、グループボックスの表示/非表示、スタックドウィジェットによるコンテンツの切り替え、アニメーションによる視覚的な変化など。
    • 利点
      モーダルウィンドウによる操作の中断を避け、アプリケーションの流れをスムーズに保てます。
    • 欠点
      複雑な入力や多くの情報を扱う場合には、インターフェースが煩雑になる可能性があります。
    • 使用例
      詳細設定の展開/格納、ステップごとの入力フォーム、エラーメッセージのインライン表示など。
    • 概念的なコード例 (具体的な実装はUI構造に依存します)
      #include <QApplication>
      #include <QWidget>
      #include <QPushButton>
      #include <QVBoxLayout>
      #include <QGroupBox>
      #include <QCheckBox>
      #include <QLabel>
      
      int main(int argc, char *argv[]) {
          QApplication a(argc, argv);
          QWidget mainWindow;
          QVBoxLayout *mainLayout = new QVBoxLayout(&mainWindow);
      
          QGroupBox *settingsGroup = new QGroupBox("詳細設定", &mainWindow);
          QVBoxLayout *settingsLayout = new QVBoxLayout(settingsGroup);
          QCheckBox *option1 = new QCheckBox("オプション 1", settingsGroup);
          QCheckBox *option2 = new QCheckBox("オプション 2", settingsGroup);
          settingsLayout->addWidget(option1);
          settingsLayout->addWidget(option2);
          settingsGroup->hide(); // 初期状態では非表示
      
          QPushButton *showSettingsButton = new QPushButton("詳細設定を表示/非表示", &mainWindow);
          QObject::connect(showSettingsButton, &QPushButton::clicked, [&]() {
              settingsGroup->setVisible(!settingsGroup->isVisible());
          });
      
          QLabel *mainLabel = new QLabel("メインコンテンツ", &mainWindow);
      
          mainLayout->addWidget(mainLabel);
          mainLayout->addWidget(showSettingsButton);
          mainLayout->addWidget(settingsGroup);
          mainWindow.setLayout(mainLayout);
          mainWindow.show();
          return a.exec();
      }
      
  3. ウィザード (QWizard)

    • 説明
      複数のステップからなる複雑なタスクを、順を追ってユーザーに入力させるための専用のクラスです。モーダルなウィンドウとして表示されますが、ステップごとに情報を整理し、戻る/進むボタンを提供することで、ユーザーエクスペリエンスを向上させます。
    • 利点
      複雑な処理を段階的に案内し、ユーザーの混乱を防ぎます。
    • 欠点
      単純な入力や確認にはオーバースペックになることがあります。
    • 使用例
      アプリケーションの初期設定、ソフトウェアのインストール、複雑なデータ入力フォームなど。