PySide6でデスクトップアプリを作る #2 ウィジェットとレイアウト — 画面を組み立てる

読了 6分

前回はボタン1つがウィンドウいっぱいに広がるアプリを作りました。実際のアプリの画面では、入力欄、リスト、複数のボタンがそれぞれ決まった位置に置かれ、ウィンドウのサイズを変えても配置が自然についてくる必要があります。今回はその配置を担う2つの材料、ウィジェットレイアウトを扱います。

全7回で構成します。

  • #1 PySide6とは — QtとPythonでデスクトップアプリ
  • #2 ウィジェットとレイアウト — 画面を組み立てる ← この記事
  • #3 シグナルとスロット — イベント処理の核心
  • #4 Qt DesignerとUIファイル — 画面を描いて読み込む
  • #5 モデルとビュー — リスト・テーブルにデータをつなぐ
  • #6 スレッドとタイマー — 止まらないUI
  • #7 パッケージングと配布 — PyInstallerで実行ファイルを作る

今回が終わるとToDoアプリの画面の骨組みが完成します。動作はまだありませんが、次回シグナルをつなげばすぐに動き出す構造です。

よく使うウィジェットのカタログ #

ウィジェットは画面を構成する部品です。Qtには数十種類ありますが、業務ツール水準のアプリはほとんど次の7つで組み立てられます。

ウィジェット用途
QLabelテキストや画像の表示。編集不可
QLineEdit1行のテキスト入力
QPushButtonクリックするボタン
QCheckBoxオン・オフを切り替えるチェックボックス
QComboBoxドロップダウンの選択リスト
QTextEdit複数行のテキスト入力と表示
QSpinBox上下の矢印つき数値入力

これに項目のリストを表示するQListWidgetを加えれば、今回の材料はすべてそろいます。各ウィジェットの詳細なオプションは必要なときに公式ドキュメントで調べればよく、いま重要なのはどんな部品が存在するかを知っておくことです。

座標配置の落とし穴とレイアウトマネージャー #

ウィジェットを配置するもっとも原始的な方法は座標指定です。button.move(50, 100)のようにピクセル位置を直接打つ方式ですが、この方式は実戦ではすぐに破綻します。ウィンドウのサイズを変えるとウィジェットがついてこず、フォントサイズが異なる環境ではウィジェットが重なったり切れたりします。

そこでQtは配置をレイアウトマネージャーに任せます。ウィジェットをレイアウトに追加するだけで、ウィンドウのサイズが変わるたびにレイアウトが各ウィジェットの位置とサイズを計算し直します。基本のレイアウトは4つです。

4つの基本レイアウト
from PySide6.QtWidgets import (
    QVBoxLayout,   # 上から下へ積む
    QHBoxLayout,   # 左から右へ並べる
    QGridLayout,   # 行と列の格子に配置
    QFormLayout,   # ラベルと入力のペアを2列に整列
)

基本はQVBoxLayoutQHBoxLayoutです。格子が必要な電卓画面はQGridLayout、設定画面のように「名前: 入力欄」のペアが繰り返されるフォームにはQFormLayoutが合います。

QGridLayoutとQFormLayoutを味見する
# 格子配置 — addWidget(ウィジェット, 行, 列)
grid = QGridLayout()
grid.addWidget(QPushButton("7"), 0, 0)
grid.addWidget(QPushButton("8"), 0, 1)
grid.addWidget(QPushButton("9"), 0, 2)

# フォーム配置 — addRow(ラベル, 入力ウィジェット)
form = QFormLayout()
form.addRow("名前", QLineEdit())
form.addRow("ポート", QSpinBox())

入れ子 — レイアウトの中にレイアウト #

実戦の画面はレイアウト1つでは終わりません。ToDoアプリの画面を設計してみると、こう分かれます。

ToDoアプリの画面設計
┌─────────────────────────────┐
│ [ 入力欄          ] [追加]   │  ← 横並び (QHBoxLayout)
├─────────────────────────────┤
│                             │
│  ToDoリスト (QListWidget)    │  ← 中央、残りの空間すべて
│                             │
├─────────────────────────────┤
│      [選択を削除] [すべて削除] │  ← 横並び (QHBoxLayout)
└─────────────────────────────┘
        ↑ 全体を縦に積む (QVBoxLayout)

横の列2つとリスト1つを縦のレイアウトに積む構造です。コードに移すと次のようになります。

main.py — ToDoアプリの画面の骨組み
import sys

from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget,
    QVBoxLayout, QHBoxLayout,
    QLineEdit, QPushButton, QListWidget,
)


class TodoWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("ToDo管理")
        self.resize(400, 500)

        # 上部の入力列 — 入力欄 + 追加ボタン
        self.todo_input = QLineEdit()
        self.todo_input.setPlaceholderText("ToDoを入力してください")
        self.add_button = QPushButton("追加")
        input_row = QHBoxLayout()
        input_row.addWidget(self.todo_input)
        input_row.addWidget(self.add_button)

        # 中央のリスト
        self.todo_list = QListWidget()

        # 下部のボタン列 — 右寄せ
        self.delete_button = QPushButton("選択を削除")
        self.clear_button = QPushButton("すべて削除")
        button_row = QHBoxLayout()
        button_row.addStretch()
        button_row.addWidget(self.delete_button)
        button_row.addWidget(self.clear_button)

        # 全体の組み立て — 縦に積む
        layout = QVBoxLayout()
        layout.addLayout(input_row)
        layout.addWidget(self.todo_list)
        layout.addLayout(button_row)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = TodoWindow()
    window.show()
    sys.exit(app.exec())

実行すると設計図どおりのウィンドウが表示され、ウィンドウを広げるとリスト領域が一緒に広がります。組み立てのルールは3つです。

  • ウィジェットはaddWidget、レイアウトはaddLayout — レイアウトの中にレイアウトを入れるときはaddLayoutを使います。
  • レイアウトはウィジェットに載せて初めて画面に出ますQMainWindowの中央にはウィジェットしか入れられないため、空のQWidgetにsetLayoutでレイアウトを載せ、そのウィジェットをsetCentralWidgetに渡します。
  • ウィジェットはselfの属性として保持しますself.todo_inputのように持っておいたウィジェットだけが、次回シグナルをつないだり値を読んだりできます。

余白、整列、残りの空間 #

配置を整える3つの道具を確認します。

  • stretch — 下部のボタン列のaddStretch()は、空きスペースを占めるバネです。ボタンの前に入れたため、ボタンが右へ押されて整列します。
  • 余白と間隔layout.setContentsMargins(12, 12, 12, 12)はレイアウトの外側の余白、layout.setSpacing(8)はウィジェット同士の間隔を指定します。
  • サイズの比率layout.addWidget(self.todo_list, stretch=1)のようにstretch値を与えると、ウィンドウが大きくなったときの残りの空間をそのウィジェットが優先的に受け取ります。
ヒント
レイアウトが意図どおりにならないときは、構造を紙に先に描いてみるのがいちばん速い方法です。画面を横の列と縦のまとまりに分割する設計が終われば、コードはその図をそのまま書き写す作業になります。

まとめ #

今回の核心は3つです。

  • 画面はウィジェットという部品とレイアウトという配置ルールで組み立て、座標指定は使いません。
  • 実戦の画面はQHBoxLayoutとQVBoxLayoutの入れ子でほとんど表現でき、レイアウトは空のQWidgetに載せてsetCentralWidgetでウィンドウに載せます。
  • addStretch、余白、stretch比率で配置を整えます。

いまのToDoアプリはボタンを押しても何も起きません。次回の「PySide6でデスクトップアプリを作る #3 シグナルとスロット — イベント処理の核心」でQtのイベント処理の核心モデルであるシグナルとスロットを学び、この画面に実際の動作をつなぎます。

X