Qt QWidget徹底解説:GUIプログラミングの基本を学ぶ

2025-05-16

QWidget (クラス) とは?

QWidgetクラスは、Qtフレームワークにおけるユーザーインターフェース(UI)オブジェクトの全ての基底クラスです。簡単に言うと、GUI(Graphical User Interface)を構成する目に見えるあらゆる要素の土台となるのがQWidgetです。

具体的には、以下のようなものがQWidgetから派生しています。

  • ウィンドウそのもの (QMainWindow, QDialogなど)
  • チェックボックス (QCheckBox)
  • テキスト入力欄 (QLineEdit, QTextEdit)
  • ラベル (QLabel)
  • ボタン (QPushButton)

つまり、QtでGUIアプリケーションを開発する際には、ほとんどの場合、直接的または間接的にQWidgetクラスを扱うことになります。

QWidgetの主な役割と機能

QWidgetは、ウィジェットとしての基本的な機能を提供します。

    • 画面上に自分自身を描画する能力を持ちます。
    • paintEvent()という仮想関数をオーバーライドすることで、ウィジェットの見た目を自由にカスタマイズできます。
  1. イベントハンドリング (Event Handling)

    • マウスのクリック、キーボード入力、ウィンドウのリサイズなど、様々なユーザー入力やシステムイベントを受け取り、処理する機能を提供します。
    • mousePressEvent(), keyPressEvent(), resizeEvent()などの仮想関数をオーバーライドすることで、特定のイベントに対する動作を定義できます。
  2. ジオメトリ管理 (Geometry Management)

    • 自身の位置 (pos) や大きさ (size) を管理します。
    • 親ウィジェットに対する相対的な位置で配置されます。
  3. プロパティと状態 (Properties and State)

    • 表示/非表示 (visible)、有効/無効 (enabled)、背景色 (palette)、フォント (font) など、ウィジェットの様々な状態を管理するプロパティを持っています。
    • これらのプロパティは、親ウィジェットから子ウィジェットへと継承されることがあります。
  4. 親子関係 (Parent-Child Relationship)

    • QWidgetQObjectを継承しており、QObjectの親子関係の仕組みがそのまま適用されます。
    • さらに、QWidget独自のウィジェットとしての親子関係も持ちます。
      • 子ウィジェットは親ウィジェットの内部に描画されます。
      • 子ウィジェットの位置は親ウィジェットに対する相対座標になります。
      • 親ウィジェットが非表示になると、その子ウィジェットも自動的に非表示になります。
    • 親を持たないQWidgetは、トップレベルウィンドウ(独立したウィンドウ)として表示されます。
  • プラットフォーム独立性
    QWidgetは、OSごとのUIの違いを抽象化し、Windows、macOS、Linuxなどの異なるプラットフォームで一貫した見た目と動作を提供する基盤となります。
  • 再利用性
    独自のカスタムウィジェットを作成する場合も、QWidgetを継承して機能を拡張することで、既存のQtの仕組みに統合されたUI要素として扱えます。
  • GUIの基本要素
    QtアプリケーションのGUIは、すべてQWidgetとその派生クラスの組み合わせで構築されます。


QWidgetはQtアプリケーションのUIの根幹をなすため、様々な側面で問題が発生する可能性があります。ここでは、よく遭遇する問題とその解決策をいくつか紹介します。

ウィジェットが表示されない・見えない

これは最もよくある問題の一つです。

  • 考えられる原因とトラブルシューティング

    1. QApplicationの初期化忘れ

      • 原因
        Qtアプリケーションは、UIを表示する前に必ずQApplicationインスタンスを作成し、exec_()(またはexec())メソッドを呼び出す必要があります。これがないとイベントループが開始されず、ウィジェットは表示されません。
      • 解決策
        main関数(またはエントリポイントとなる関数)の先頭でQApplicationを初期化し、最後にapp.exec_()(Pythonの場合)またはapp.exec()(C++の場合)を呼び出しているか確認してください。

      <!-- end list -->

      # Python (PyQt/PySide)
      import sys
      from PySide6.QtWidgets import QApplication, QWidget
      
      if __name__ == "__main__":
          app = QApplication(sys.argv) # これがないとウィジェットは表示されない
          window = QWidget()
          window.show()
          sys.exit(app.exec())
      
      // C++
      #include <QApplication>
      #include <QWidget>
      
      int main(int argc, char *argv[]) {
          QApplication app(argc, argv); // これがないとウィジェットは表示されない
          QWidget window;
          window.show();
          return app.exec();
      }
      
    2. 親ウィジェットの未設定または不適切な設定

      • 原因
        子ウィジェットは親ウィジェットの中に表示されます。親が設定されていない場合(またはトップレベルウィンドウでない場合)、表示されないことがあります。また、親ウィジェットが非表示になっている場合、子ウィジェットも表示されません。
      • 解決策
        • 子ウィジェットを作成する際に、親ウィジェットを引数として渡すか、setParent()メソッドを使用します。
        • 親ウィジェットがshow()されているか、また、他のウィジェットによって隠されていないか確認します。
        • 特に、QScrollAreaQDockWidgetなどのコンテナウィジェットにウィジェットをセットする場合、それらのコンテナウィジェット自身のsetWidget()メソッドで正しくセットされているか確認します。レイアウトを設定する前にsetWidget()を呼び出す必要がある場合があります。
    3. レイアウトの未設定または問題

      • 原因
        QWidgetに直接子ウィジェットを追加しても、明示的にレイアウトを設定しない限り、それらは適切な位置に配置されず、重なって表示されたり、一部しか見えなかったりします。
      • 解決策
        QHBoxLayout, QVBoxLayout, QGridLayoutなどのレイアウトマネージャーを使用して、ウィジェットを親ウィジェットに適切に配置します。setLayout()メソッドでウィジェットにレイアウトを割り当てます。
      # レイアウトを使ってウィジェットを配置する例
      from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton
      
      if __name__ == "__main__":
          app = QApplication(sys.argv)
          window = QWidget()
          layout = QVBoxLayout(window) # 親ウィジェットを指定してレイアウトを作成
      
          button1 = QPushButton("ボタン1")
          button2 = QPushButton("ボタン2")
      
          layout.addWidget(button1)
          layout.addWidget(button2)
      
          window.setLayout(layout) # ウィジェットにレイアウトをセット
          window.show()
          sys.exit(app.exec())
      
    4. ウィジェットのサイズポリシーやサイズヒントが不適切

      • 原因
        カスタムウィジェットを作成した場合や、レイアウトが期待通りに動作しない場合、sizeHint(), minimumSizeHint(), sizePolicy()の実装が不適切である可能性があります。
      • 解決策
        • カスタムウィジェットで、sizeHint()をオーバーライドして推奨サイズを返し、minimumSizeHint()をオーバーライドして最小サイズを返します。
        • setSizePolicy()を使用して、ウィジェットがレイアウト内でどのようにスペースを要求するかを指定します(例: QSizePolicy.ExpandingQSizePolicy.Fixedなど)。
    5. setVisible(false)の呼び出し

      • 原因
        誤ってsetVisible(false)を呼び出している場合があります。
      • 解決策
        デバッグ中にisVisible()をチェックしたり、setVisible(true)を明示的に呼び出したりして、ウィジェットの可視性を確認します。
    • コードでウィジェットを作成し、show()を呼び出しても、画面に何も表示されない。
    • ウィジェットが他のウィジェットの下に隠れてしまう。
    • レイアウトにウィジェットを追加したのに、適切な場所に表示されない。

イベントが処理されない・期待通りに動作しない

ボタンがクリックされても何も起こらない、マウスイベントが検出されないなど。

  • 考えられる原因とトラブルシューティング

    1. シグナルとスロットの接続ミス

      • 原因
        シグナルとスロットの接続が正しく行われていない(スペルミス、引数の不一致など)。
      • 解決策
        • 接続の構文が正しいか確認します。
        • スロット関数が正しい引数を受け取っているか確認します。
        • デバッグ出力(qDebug()またはprint())で、シグナルが発火しているか、スロットが呼び出されているかを確認します。
    2. イベントフィルターの使用

      • 原因
        親ウィジェットがイベントフィルターをインストールしており、子ウィジェットのイベントを消費してしまっている場合があります。
      • 解決策
        イベントフィルターの実装を確認し、必要に応じてイベントを無視するか、目的のウィジェットに転送するように修正します。
    3. イベントのオーバーライド不足または基底クラスの呼び出し忘れ

      • 原因
        mousePressEvent()などのイベントハンドラをオーバーライドしたが、基底クラスの同じメソッドを呼び出していないため、デフォルトの動作が失われている。
      • 解決策
        オーバーライドしたイベントハンドラ内で、必ず基底クラスの対応するメソッドを呼び出すようにします(例: event.accept()event.ignore()でイベントの伝播を制御する必要がある場合を除く)。
      // C++
      void MyWidget::mousePressEvent(QMouseEvent *event) {
          // カスタム処理
          qDebug() << "Mouse pressed!";
      
          // イベントをさらに親ウィジェットに伝播させるために基底クラスのメソッドを呼び出す
          QWidget::mousePressEvent(event); 
      }
      
    4. フォーカス関連の問題

      • 原因
        キーボードイベントなど、フォーカスが必要なイベントが、目的のウィジェットにフォーカスが当たっていないために発生しない。
      • 解決策
        setFocus()を呼び出してウィジェットに明示的にフォーカスをセットしたり、focusPolicy()を適切に設定したりします。
  • エラーの兆候

    • シグナルとスロットの接続が機能しない。
    • mousePressEvent()などのイベントハンドラが呼び出されない。

描画の問題

ウィジェットの描画が期待通りでない、ちらつきが発生する、一部が描画されないなど。

  • 考えられる原因とトラブルシューティング

    1. paintEvent()の呼び出しタイミング

      • 原因
        paintEvent()は、Qtのイベントループによって自動的に呼び出されます。手動で直接呼び出してはいけません。
      • 解決策
        • ウィジェットの再描画が必要な場合は、update()またはrepaint()を呼び出します(通常はupdate()が推奨されます。update()は再描画をスケジュールし、repaint()は即座に再描画します)。
        • QPainter::begin: Widget painting can only begin as a result of a paintEventのようなエラーが出る場合、paintEventの外部でQPainterを初期化しようとしている可能性があります。
    2. QPainterの初期化と終了

      • 原因
        QPainterオブジェクトは、paintEvent()内で作成し、描画が完了したら破棄する必要があります。
      • 解決策
        QPainterpaintEvent()関数のスコープ内で作成し、関数が終了すると自動的に破棄されるようにします。
      // C++
      void MyWidget::paintEvent(QPaintEvent *event) {
          QPainter painter(this); // ウィジェット上でペインタを初期化
          // 描画処理
          painter.drawRect(rect());
          // painterはスコープを抜けると自動的に破棄される
      }
      
    3. 背景の消去(ダブルバッファリング)

      • 原因
        QPaintEventの処理中に背景が適切に消去されていないと、前の描画が残ってちらつきの原因となることがあります。
      • 解決策
        setAttribute(Qt::WA_OpaquePaintEvent)を設定して、ウィジェットが完全に不透明であり、背景を消去する必要がないことをQtに知らせるか、またはpaintEvent()内で背景を明示的に塗りつぶします。
    4. Zオーダー(重なり順)の問題

      • 原因
        複数のウィジェットが重なる場合、Zオーダーによってどれが手前に表示されるかが決まります。意図しないウィジェットが手前に来ている可能性があります。
      • 解決策
        • raise()lower()メソッドを使用して、ウィジェットのZオーダーを変更できます。
        • レイアウトを使用している場合、レイアウトがZオーダーを管理しますが、カスタムペインティングで問題が発生することもあります。
  • エラーの兆候

    • paintEvent()をオーバーライドしたのに、カスタム描画が表示されない。
    • ウィジェットがリサイズされたり、他のウィンドウに覆われた後に、正しく再描画されない。
    • テキストや画像が欠落する。

メモリリークとクラッシュ (Segmentation Fault)

  • 考えられる原因とトラブルシューティング

    1. オブジェクトの親子関係の不整合

      • 原因
        Qtのオブジェクトツリー(親子関係)が正しく構築されていない場合、親が子を解放しようとしてすでに解放されたメモリにアクセスしたり、子が解放されずにメモリリークを引き起こしたりします。
      • 解決策
        Qtのオブジェクトは、親が設定されている場合、親が破棄されるときに自動的に子を破棄します。手動でdelete(C++)やPythonのガベージコレクションに頼る場合、適切なタイミングでオブジェクトを解放しているか確認します。特に、ヒープに作成したオブジェクト(newで作成)は、適切なタイミングでdeleteするか、親を設定する必要があります。
    2. イベントループの終了

      • 原因
        QApplication::exec()が終了する前に、UI要素が破棄されないと問題が発生することがあります。
      • 解決策
        通常はsys.exit(app.exec())(Python)やreturn app.exec();(C++)のように、イベントループが正常に終了するようにします。
    3. 無効なポインタや参照の使用

      • 原因
        すでに解放されたウィジェットのポインタ(C++)や参照(Python)を使用しようとした場合。
      • 解決策
        オブジェクトが有効であることを確認してから操作を行います。QPointer(C++)やQWeakPointer(C++11以降)を使用すると、破棄されたオブジェクトへのアクセスをより安全に管理できます。
  • エラーの兆候

    • アプリケーションが突然終了する。
    • メモリ使用量が増加し続ける。
    • デバッグ時にセグメンテーションフォールトや不正なアクセスエラーが表示される。

Qt Designerとの連携の問題

  • 考えられる原因とトラブルシューティング

    1. レイアウトの適用忘れ

      • 原因
        Designerでウィジェットを配置した後、トップレベルのウィジェット(例: QMainWindowcentralWidgetQDialog)に明示的にレイアウトを適用していない。
      • 解決策
        DesignerでUIファイル(.ui)を編集する際、メインのウィジェットを選択し、右クリックして「Layout」メニューから適切なレイアウト(例: Lay Out VerticallyLay Out HorizontallyLay Out in a Gridなど)を選択し、そのレイアウトを適用します。これにより、ウィジェットがリサイズされたときに正しく配置されます。
    2. カスタムウィジェットのプロモーション

      • 原因
        Designerでカスタムウィジェットを使用する場合、そのウィジェットを「プロモート」する設定を忘れている。
      • 解決策
        Designerでプレースホルダーのウィジェットを選択し、右クリックメニューから「Promote to...」を選択し、実際のカスタムクラス名を設定します。
  • エラーの兆候

    • Designerで作成したUIが、コードでロードすると期待通りに表示されない。
    • ウィジェットがDesignerでは正常に見えるのに、実行時にレイアウトが崩れる。

一般的なトラブルシューティングのヒント

  • Qtのドキュメントを参照する
    QWidgetおよび関連クラスの公式ドキュメントは非常に充実しています。各メソッドの動作や要件を理解するために、積極的に活用してください。
  • 最小限の再現コードを作成する
    問題が発生したコード全体ではなく、その問題を再現できる最小限のコードスニペットを作成してみます。これにより、問題の原因を絞り込むことができます。
  • デバッガを使用する
    ステップ実行や変数の値の確認は、問題の特定に非常に有効です。
  • エラーメッセージをよく読む
    Qtは非常に詳細なエラーメッセージを出力することが多いです。特にコンソールに出力される警告やエラーメッセージは、問題解決の大きなヒントになります。


QWidgetはQtのGUIにおける基本要素であり、様々な場面で利用されます。ここでは、基本的な使い方から、少し発展的なカスタムウィジェットの作成まで、いくつかの例をC++とPython (PyQt/PySide) の両方で示します。

基本的なウィンドウの作成

最もシンプルなQWidgetの使用例です。QWidgetをトップレベルウィンドウとして表示します。

C++ (main.cpp)

#include <QApplication> // QApplicationはQtアプリケーションのイベントループを管理します
#include <QWidget>      // QWidgetクラスをインクルードします

int main(int argc, char *argv[]) {
    // 1. QApplicationインスタンスの作成: Qtアプリケーションの初期化とイベントループの管理
    QApplication app(argc, argv);

    // 2. QWidgetインスタンスの作成: これがメインウィンドウになります
    QWidget window;

    // 3. ウィンドウのタイトルを設定
    window.setWindowTitle("シンプルなQtウィンドウ");

    // 4. ウィンドウのサイズを設定 (幅x高さ)
    window.resize(300, 200);

    // 5. ウィンドウを表示
    window.show();

    // 6. アプリケーションのイベントループを開始: これがないとウィンドウは表示されません
    return app.exec();
}

Python (main.py)

import sys
from PySide6.QtWidgets import QApplication, QWidget # PyQt5の場合は PyQt5.QtWidgets

if __name__ == "__main__":
    # 1. QApplicationインスタンスの作成
    app = QApplication(sys.argv)

    # 2. QWidgetインスタンスの作成
    window = QWidget()

    # 3. ウィンドウのタイトルを設定
    window.setWindowTitle("シンプルなQtウィンドウ")

    # 4. ウィンドウのサイズを設定
    window.resize(300, 200)

    # 5. ウィンドウを表示
    window.show()

    # 6. アプリケーションのイベントループを開始
    sys.exit(app.exec())

解説

  • show()でウィンドウを表示し、app.exec()(C++)またはapp.exec()(Python)でイベントループを開始します。これにより、ユーザーインタラクション(クリック、キー入力など)を処理できるようになります。
  • QWidgetのインスタンスを作成し、setWindowTitle()でタイトル、resize()でサイズを設定します。
  • QApplicationはQtアプリケーションの初期化とイベントループの管理を行います。GUIアプリケーションには必須です。

レイアウトを使ったウィジェットの配置

QWidgetは、子ウィジェットを配置するためにレイアウトマネージャーと組み合わせて使うのが一般的です。ここでは、ボタンを縦に並べる例を示します。

C++ (main.cpp)

#include <QApplication>
#include <QWidget>
#include <QVBoxLayout> // 垂直レイアウト
#include <QPushButton> // ボタン

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

    QWidget window;
    window.setWindowTitle("レイアウトを使ったウィンドウ");
    window.resize(300, 200);

    // 1. QVBoxLayout (垂直レイアウト) インスタンスの作成
    QVBoxLayout *layout = new QVBoxLayout(&window); // 親ウィジェットを指定

    // 2. ボタンの作成
    QPushButton *button1 = new QPushButton("ボタン 1");
    QPushButton *button2 = new QPushButton("ボタン 2");
    QPushButton *button3 = new QPushButton("終了");

    // 3. レイアウトにボタンを追加
    layout->addWidget(button1);
    layout->addWidget(button2);
    layout->addWidget(button3);

    // 4. ウィンドウにレイアウトを設定
    // QWidgetは自動的にレイアウトの所有権を持つため、delete layout; は不要
    window.setLayout(layout); 

    // 5. ボタンと終了シグナルを接続
    // QObject::connect(Sender, &Sender::signal, Receiver, &Receiver::slot);
    QObject::connect(button3, &QPushButton::clicked, &app, &QApplication::quit);

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

Python (main.py)

import sys
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton

if __name__ == "__main__":
    app = QApplication(sys.argv)

    window = QWidget()
    window.setWindowTitle("レイアウトを使ったウィンドウ")
    window.resize(300, 200)

    # 1. QVBoxLayout (垂直レイアウト) インスタンスの作成
    layout = QVBoxLayout(window) # 親ウィジェットを指定

    # 2. ボタンの作成
    button1 = QPushButton("ボタン 1")
    button2 = QPushButton("ボタン 2")
    button3 = QPushButton("終了")

    # 3. レイアウトにボタンを追加
    layout.addWidget(button1)
    layout.addWidget(button2)
    layout.addWidget(button3)

    # 4. ウィンドウにレイアウトを設定 (Pythonでは layout(window) で自動的にセットされる)
    # window.setLayout(layout) は省略可能だが、明示的に書いても問題ない

    # 5. ボタンと終了シグナルを接続
    button3.clicked.connect(app.quit)

    window.show()
    sys.exit(app.exec())

解説

  • QPushButtonclickedシグナルとQApplicationquitスロットを接続し、"終了"ボタンがクリックされたらアプリケーションを終了するようにします。
  • 最後にsetLayout()でウィジェットにレイアウトを設定します。Pythonでは、レイアウトのコンストラクタで親ウィジェットを指定した場合、setLayout()は自動的に呼び出されるため省略可能です。
  • addWidget()メソッドでウィジェットをレイアウトに追加します。
  • QVBoxLayout(垂直ボックスレイアウト)を使って、追加されたウィジェットを縦に並べます。

イベントハンドリングを持つカスタムウィジェットの作成

QWidgetを継承して独自のカスタムウィジェットを作成し、イベントハンドラをオーバーライドする例です。ここでは、クリックされたときにメッセージを出す簡単なウィジェットを作成します。

C++ (mycustomwidget.h)

#ifndef MYCUSTOMWIDGET_H
#define MYCUSTOMWIDGET_H

#include <QWidget>
#include <QDebug> // デバッグ出力用

class MyCustomWidget : public QWidget {
    Q_OBJECT // シグナルとスロットを使用するために必要

public:
    explicit MyCustomWidget(QWidget *parent = nullptr); // コンストラクタ

protected:
    // マウスプレスイベントをオーバーライド
    void mousePressEvent(QMouseEvent *event) override; 
    // 描画イベントをオーバーライド
    void paintEvent(QPaintEvent *event) override;
};

#endif // MYCUSTOMWIDGET_H

C++ (mycustomwidget.cpp)

#include "mycustomwidget.h"
#include <QMouseEvent> // マウスイベントのクラス
#include <QPainter>    // 描画用のクラス
#include <QBrush>      // ブラシ(塗りつぶし)用のクラス
#include <QPen>        // ペン(線)用のクラス

MyCustomWidget::MyCustomWidget(QWidget *parent) : QWidget(parent) {
    // ウィジェットの背景を自動で塗りつぶすように設定
    setAutoFillBackground(true); 
    // 背景色を設定(例:薄いグレー)
    QPalette palette = this->palette();
    palette.setColor(QPalette::Window, Qt::lightGray);
    setPalette(palette);
}

void MyCustomWidget::mousePressEvent(QMouseEvent *event) {
    // マウスの左ボタンがクリックされたらメッセージを出力
    if (event->button() == Qt::LeftButton) {
        qDebug() << "カスタムウィジェットがクリックされました!";
    }
    // 基底クラスのイベントハンドラを呼び出す
    QWidget::mousePressEvent(event); 
}

void MyCustomWidget::paintEvent(QPaintEvent *event) {
    Q_UNUSED(event); // 未使用の引数に対する警告を抑制

    QPainter painter(this); // ウィジェット上で描画を開始
    
    // 背景を塗りつぶす (autoFillBackgroundがtrueなら不要な場合もある)
    painter.fillRect(rect(), palette().window());

    // カスタム描画: 赤い円を描画
    QBrush brush(Qt::red);
    QPen pen(Qt::blue, 2); // 青い線、太さ2px

    painter.setBrush(brush);
    painter.setPen(pen);

    // ウィジェットの中央に円を描画
    int radius = qMin(width(), height()) / 3;
    painter.drawEllipse(rect().center(), radius, radius);

    // テキストを描画
    painter.setPen(Qt::black); // テキストの色
    painter.setFont(QFont("Arial", 16)); // フォント
    painter.drawText(rect(), Qt::AlignCenter, "カスタムウィジェット");

    // QPainterはスコープを抜けると自動的に終了します
}

C++ (main.cpp)

#include <QApplication>
#include <QMainWindow> // メインウィンドウとしてQMainwindowを使用することもできます
#include <QVBoxLayout>
#include "mycustomwidget.h" // 作成したカスタムウィジェットをインクルード

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

    // メインウィンドウを作成
    QMainWindow mainWindow;
    mainWindow.setWindowTitle("カスタムウィジェットのテスト");
    mainWindow.resize(400, 300);

    // 中央ウィジェットを作成し、その中にレイアウトを設定
    QWidget *centralWidget = new QWidget(&mainWindow);
    QVBoxLayout *layout = new QVBoxLayout(centralWidget);

    // カスタムウィジェットのインスタンスを作成
    MyCustomWidget *customWidget = new MyCustomWidget(centralWidget);
    customWidget->setMinimumSize(100, 100); // 最小サイズを設定

    // レイアウトにカスタムウィジェットを追加
    layout->addWidget(customWidget);

    // 中央ウィジェットをメインウィンドウに設定
    mainWindow.setCentralWidget(centralWidget);

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

Python (mycustomwidget.py)

from PySide6.QtWidgets import QWidget, QApplication, QVBoxLayout
from PySide6.QtGui import QPainter, QBrush, QPen, QFont, QColor, QPalette
from PySide6.QtCore import Qt, QPoint, QRect, QSize
import sys

class MyCustomWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # ウィジェットの背景を自動で塗りつぶすように設定
        self.setAutoFillBackground(True)
        # 背景色を設定(例:薄いグレー)
        palette = self.palette()
        palette.setColor(QPalette.Window, QColor(Qt.lightGray))
        self.setPalette(palette)

    def mousePressEvent(self, event):
        # マウスの左ボタンがクリックされたらメッセージを出力
        if event.button() == Qt.LeftButton:
            print("カスタムウィジェットがクリックされました!")
        # 基底クラスのイベントハンドラを呼び出す
        super().mousePressEvent(event)

    def paintEvent(self, event):
        painter = QPainter(self) # ウィジェット上で描画を開始
        
        # 背景を塗りつぶす (autoFillBackgroundがTrueなら不要な場合もある)
        painter.fillRect(self.rect(), self.palette().window())

        # カスタム描画: 赤い円を描画
        brush = QBrush(Qt.red)
        pen = QPen(Qt.blue, 2) # 青い線、太さ2px

        painter.setBrush(brush)
        painter.setPen(pen)

        # ウィジェットの中央に円を描画
        # qMinはPythonのmin()に相当
        radius = min(self.width(), self.height()) // 3 
        center = self.rect().center()
        painter.drawEllipse(center, radius, radius)

        # テキストを描画
        painter.setPen(Qt.black) # テキストの色
        painter.setFont(QFont("Arial", 16)) # フォント
        painter.drawText(self.rect(), Qt.AlignCenter, "カスタムウィジェット")

        painter.end() # 描画を終了 (PythonではQPainterオブジェクトが破棄されると自動で呼ばれるが、明示しても良い)

if __name__ == "__main__":
    app = QApplication(sys.argv)

    # メインウィンドウを作成
    mainWindow = QWidget() # QMainWindowでも良い
    mainWindow.setWindowTitle("カスタムウィジェットのテスト")
    mainWindow.resize(400, 300)

    # レイアウトを作成
    layout = QVBoxLayout(mainWindow)

    # カスタムウィジェットのインスタンスを作成
    customWidget = MyCustomWidget(mainWindow)
    customWidget.setMinimumSize(100, 100) # 最小サイズを設定

    # レイアウトにカスタムウィジェットを追加
    layout.addWidget(customWidget)

    mainWindow.show()
    sys.exit(app.exec())
  • setAutoFillBackground(True) / setPalette()
    ウィジェットの背景色を設定する一般的な方法です。
  • paintEvent(QPaintEvent *event)
    ウィジェットの描画が必要なときに呼び出される仮想関数をオーバーライドします。この関数内でQPainterを使用して、図形やテキストなどを描画します。
    • QPainter(this): this(現在のウィジェット)上で描画を行うQPainterオブジェクトを作成します。
    • fillRect(), setBrush(), setPen(), drawEllipse(), drawText()など、様々な描画関数があります。
    • 重要
      paintEvent()は、Qtのイベントループによって自動的に呼び出されるため、直接呼び出してはいけません。ウィジェットを再描画したい場合は、update()メソッドを呼び出します(update()は再描画をスケジュールする)。
  • mousePressEvent(QMouseEvent *event)
    マウスがクリックされたときに呼び出される仮想関数をオーバーライドします。event->button()でどのボタンが押されたか確認できます。
  • コンストラクタ
    parent引数をQWidgetのコンストラクタに渡し、親子関係を確立します。
  • Q_OBJECTマクロ (C++のみ)
    シグナル/スロットやメタオブジェクトシステムを使用するために、クラス定義の先頭にQ_OBJECTマクロが必要です。
  • 継承
    MyCustomWidgetクラスをQWidgetから継承します。


QWidget (クラス) の代替プログラミング方法

Qt Quick (QML)

QWidgetが伝統的なC++ベースのウィジェットアプローチであるのに対し、Qt Quickはよりモダンで宣言的なUI開発のためのフレームワークです。QML(Qt Markup Language)というJSONライクな宣言型言語と、JavaScript、そしてC++を組み合わせて使用します。

特徴

  • JavaScript
    QMLファイル内でJavaScriptを使ってロジックを記述できます。より複雑なロジックはC++で実装し、QMLから呼び出すことも可能です。
  • デザイナーフレンドリー
    UIデザイナーがQMLファイルを直接編集したり、Qt Design Studioのようなツールを使って視覚的にUIを構築したりしやすいように設計されています。
  • クロスプラットフォーム
    デスクトップ、モバイル(Android/iOS)、組み込みデバイスなど、幅広いプラットフォームに対応しています。
  • GPUアクセラレーション
    UIは通常、OpenGL ESなどのGPUテクノロジーを使ってレンダリングされるため、非常に滑らかで高速な描画が可能です。
  • アニメーションとトランジション
    高度なアニメーションや視覚効果を簡単に実装できます。これは、特にモバイルアプリケーションや組み込みシステムで重要です。
  • 宣言的なUI記述
    UIの構造とプロパティをQMLファイルに宣言的に記述します。これにより、UIの設計とロジックの分離が容易になります。

利用シーン

  • モバイルアプリケーション開発
  • 開発速度が重視される場合
  • アニメーションや視覚効果が豊富なアプリケーション
  • タッチスクリーンデバイス向けのUI
  • モダンなルック&フィールが求められるアプリケーション

QWidgetとの比較

  • 学習曲線
    QWidgetは既存のGUIフレームワークの経験があれば比較的理解しやすいですが、Qt QuickはQMLという新しい言語とパラダイムを学ぶ必要があります。
  • 開発アプローチ
    QWidgetは命令型(C++でUIを構築していく)であるのに対し、Qt Quickは宣言型(QMLでUIの状態を記述する)です。
  • 描画
    QWidgetは各プラットフォームのネイティブAPIで描画されるため、プラットフォームごとに微妙なルック&フィールが異なります。Qt QuickはGPUによって描画されるため、プラットフォームに依存せず、一貫した見た目を実現します。

QPainter (直接描画)

QWidgetは、基本的に内部でQPainterを使って自身を描画しています。しかし、非常に特殊な描画要件がある場合や、既存のQWidgetコンポーネントでは実現できないようなカスタムUIをゼロから作成したい場合、QPainterを直接使用してカスタムQWidgetpaintEventをオーバーライドすることで、非常に柔軟な描画が可能です。

特徴

  • パフォーマンス
    正しく最適化すれば、高い描画パフォーマンスを実現できます。
  • 描画プリミティブ
    線、図形、テキスト、画像など、豊富な描画プリミティブを提供します。
  • 完全な制御
    ピクセルレベルでUIを制御し、完全にカスタムな見た目や動作を実現できます。

利用シーン

  • 既存のウィジェットでは表現できない、完全に独自のUIコンポーネントを作成する場合
  • ゲームの2Dグラフィック
  • CADアプリケーションや画像編集ツールのような特殊なグラフィック表示
  • グラフやチャートの描画

QWidgetの派生クラスとしての利用

  • これはQWidget代替というよりは、QWidget拡張する強力な方法と考えるべきです。
  • 通常、QPainterQWidgetの派生クラスのpaintEvent()内で使用されます。つまり、QWidgetの機能(イベントハンドリング、ジオメトリ管理など)を基盤として利用しつつ、描画部分だけを完全にカスタマイズする形になります。

QGraphicsView / QGraphicsScene フレームワーク

このフレームワークは、大量の2Dグラフィックアイテムを管理・表示するために設計されています。

構成要素

  • QGraphicsItem
    シーン内の個々のグラフィックオブジェクトの基底クラス。独自のカスタムアイテムを作成することもできます。
  • QGraphicsView
    QGraphicsSceneの内容を画面に表示するためのウィジェット。複数のビューが同じシーンを表示することも可能です。
  • QGraphicsScene
    描画するグラフィックアイテム(円、四角、テキスト、カスタムアイテムなど)を格納する論理的なコンテナ。

特徴

  • ズームとパン
    シーン全体を簡単にズームしたりパンしたりする機能が組み込まれています。
  • インタラクション
    アイテムごとにイベントを処理できるため、複雑なユーザーインタラクションを簡単に実装できます。
  • 高速な描画
    大量のアイテムを効率的に管理・描画できるように最適化されています。
  • アイテムベース
    各グラフィック要素が独立したアイテムとして扱われ、それぞれが自身の位置、変換、イベント処理、描画などを持ちます。

利用シーン

  • 2Dゲーム(特に多数の独立したオブジェクトがある場合)
  • インタラクティブなデータビジュアライゼーション
  • 回路図エディタ
  • マッピングアプリケーション
  • ダイアグラムエディタ、フローチャートツール

QWidgetとの関係

  • しかし、ビューの中に表示されるQGraphicsItemQWidgetではありません。それらは独自の描画とイベント処理ロジックを持つ軽量なオブジェクトです。
  • QGraphicsView自体はQWidgetの派生クラスです。つまり、ビュー自体は通常のQtウィジェットとして扱われ、他のウィジェットと一緒にレイアウトに配置できます。

他のQtモジュールや専門的なウィジェット

Qtには、特定の目的のために特化したモジュールやウィジェットも多数存在します。これらは直接QWidgetの代替というよりは、特定のユースケースでQWidgetの一般的な派生クラスとして利用されることが多いです。

  • QOpenGLWidget
    OpenGLを使ってカスタムな3Dグラフィックなどを描画するためのウィジェット。これもQWidgetの派生クラスです。
  • Qt WebEngine Widgets
    アプリケーション内にWebコンテンツを埋め込むためのウィジェット(Webブラウザ機能)。これはQWidgetの派生クラスとして提供されます。
  • Qt Data Visualization
    3Dのデータ可視化のためのモジュール。
  • Qt Charts
    高度なグラフやチャートを簡単に作成するためのモジュール。内部的にはQGraphicsViewベースで動作します。
  • 3Dグラフィック
    QOpenGLWidgetQt Data Visualizationなどを検討します。
  • 非常に特殊な描画要件
    ピクセルレベルでの描画制御が必要な場合、QPainterを直接使用してカスタムQWidgetを開発します。
  • 大量のカスタム2Dグラフィック
    アイテムベースのインタラクティブなグラフィックが必要であれば、QGraphicsView/QGraphicsSceneフレームワークが最適です。
  • モダンなルック&フィール、アニメーション、モバイル対応
    より動的で視覚的に魅力的なUIが必要であれば、Qt Quick (QML) が有力な選択肢です。
  • 伝統的なデスクトップアプリケーション
    複雑なフォーム、データ入力、標準的なコントロール(ボタン、テキストボックス、テーブルなど)が中心であれば、QWidgetベースの開発が最も適しており、成熟しています。Qt Designerを使ったUI構築も強力です。