PySide6でデスクトップアプリを作る #2 ウィジェットとレイアウト — 画面を組み立てる
前回はボタン1つがウィンドウいっぱいに広がるアプリを作りました。実際のアプリの画面では、入力欄、リスト、複数のボタンがそれぞれ決まった位置に置かれ、ウィンドウのサイズを変えても配置が自然についてくる必要があります。今回はその配置を担う2つの材料、ウィジェットとレイアウトを扱います。
全7回で構成します。
- #1 PySide6とは — QtとPythonでデスクトップアプリ
- #2 ウィジェットとレイアウト — 画面を組み立てる ← この記事
- #3 シグナルとスロット — イベント処理の核心
- #4 Qt DesignerとUIファイル — 画面を描いて読み込む
- #5 モデルとビュー — リスト・テーブルにデータをつなぐ
- #6 スレッドとタイマー — 止まらないUI
- #7 パッケージングと配布 — PyInstallerで実行ファイルを作る
今回が終わるとToDoアプリの画面の骨組みが完成します。動作はまだありませんが、次回シグナルをつなげばすぐに動き出す構造です。
よく使うウィジェットのカタログ #
ウィジェットは画面を構成する部品です。Qtには数十種類ありますが、業務ツール水準のアプリはほとんど次の7つで組み立てられます。
| ウィジェット | 用途 |
|---|---|
QLabel | テキストや画像の表示。編集不可 |
QLineEdit | 1行のテキスト入力 |
QPushButton | クリックするボタン |
QCheckBox | オン・オフを切り替えるチェックボックス |
QComboBox | ドロップダウンの選択リスト |
QTextEdit | 複数行のテキスト入力と表示 |
QSpinBox | 上下の矢印つき数値入力 |
これに項目のリストを表示するQListWidgetを加えれば、今回の材料はすべてそろいます。各ウィジェットの詳細なオプションは必要なときに公式ドキュメントで調べればよく、いま重要なのはどんな部品が存在するかを知っておくことです。
座標配置の落とし穴とレイアウトマネージャー #
ウィジェットを配置するもっとも原始的な方法は座標指定です。button.move(50, 100)のようにピクセル位置を直接打つ方式ですが、この方式は実戦ではすぐに破綻します。ウィンドウのサイズを変えるとウィジェットがついてこず、フォントサイズが異なる環境ではウィジェットが重なったり切れたりします。
そこでQtは配置をレイアウトマネージャーに任せます。ウィジェットをレイアウトに追加するだけで、ウィンドウのサイズが変わるたびにレイアウトが各ウィジェットの位置とサイズを計算し直します。基本のレイアウトは4つです。
from PySide6.QtWidgets import (
QVBoxLayout, # 上から下へ積む
QHBoxLayout, # 左から右へ並べる
QGridLayout, # 行と列の格子に配置
QFormLayout, # ラベルと入力のペアを2列に整列
)基本はQVBoxLayoutとQHBoxLayoutです。格子が必要な電卓画面は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アプリの画面を設計してみると、こう分かれます。
┌─────────────────────────────┐
│ [ 入力欄 ] [追加] │ ← 横並び (QHBoxLayout)
├─────────────────────────────┤
│ │
│ ToDoリスト (QListWidget) │ ← 中央、残りの空間すべて
│ │
├─────────────────────────────┤
│ [選択を削除] [すべて削除] │ ← 横並び (QHBoxLayout)
└─────────────────────────────┘
↑ 全体を縦に積む (QVBoxLayout)横の列2つとリスト1つを縦のレイアウトに積む構造です。コードに移すと次のようになります。
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のイベント処理の核心モデルであるシグナルとスロットを学び、この画面に実際の動作をつなぎます。