PySide6でデスクトップアプリを作る #4 Qt DesignerとUIファイル — 画面を描いて読み込む

読了 8分

「PySide6でデスクトップアプリを作る #2 ウィジェットとレイアウト — 画面を組み立てる」では、レイアウトをすべてコードで組み立てました。ウィジェット数個の画面ならその方式で十分ですが、フォームが大きくなるとコードを読むだけでは画面の姿を思い描きにくくなり、配置を少し変えるだけでもコードの複数箇所を直すことになります。Qtはこの問題のために、画面をマウスで描く専用ツールを提供しています。今回のテーマであるQt Designerです。

全7回で構成します。

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

今回はDesignerでToDoアプリの画面を描き直し、その成果物である.uiファイルをPythonコードから読み込む2つの方式を比較したうえで、シグナル接続をコードに残すワークフローで締めくくります。

Qt Designerの起動 — インストールは済んでいます #

Qt Designerに別途のインストールは不要です。PySide6をインストールすると、コマンドラインツールとして一緒に入ります。

Qt Designerの起動
pyside6-designer

起動すると新しいフォームの選択ダイアログが開きます。独立したウィンドウを作るのでMain Windowを選びます。ダイアログ1つだけの画面なら、Dialog系のテンプレートを選んでも構いません。

画面構成 — 覚えるパネルは3つだけです #

Designerの画面は最初は複雑に見えますが、実際の作業で使うパネルは3つです。

  • ウィジェットボックス(左) — 使えるウィジェットの一覧です。ここからフォームへドラッグして配置します。
  • オブジェクトインスペクタ(右上) — フォームに載せたウィジェットのツリーです。親子関係とレイアウト構造がひと目で分かります。
  • プロパティエディタ(右下) — 選択したウィジェットのプロパティを編集します。この中で最も重要なプロパティがobjectNameです。

objectNameを強調するのには理由があります。後で.uiファイルをコードから読み込むと、各ウィジェットがobjectNameそのままのPython属性になるためです。デフォルトのpushButtonlineEditのような名前のまま残すと、コード側でどのボタンなのか区別できません。ウィジェットを載せるたびにaddButtontodoInputのように役割が伝わる名前をすぐ付ける習慣が必要です。

フォーム作成の実習 — ToDoアプリの画面 #

#2でコードで作った画面を、Designerで作り直します。手順は3つです。

  1. ウィジェットボックスからLine Editをドラッグしてフォーム上部に置き、objectNameをtodoInputにします。
  2. Push Buttonをその隣に置いてobjectNameをaddButton、textプロパティを「追加」にします。
  3. List Widgetを下に置いてobjectNameをtodoListにします。

この時点では、ウィジェットはフォーム上の絶対座標に浮いている状態です。ウィンドウの大きさを変えてもウィジェットは付いてきません。レイアウトを適用する必要があります。

レイアウトの適用とスペーサー #

Designerのレイアウトは、コードのQHBoxLayoutQVBoxLayoutと同じ概念を視覚的に適用するものです。

  1. todoInputaddButtonをまとめて選択し、ツールバーのLay Out Horizontallyを押します。2つのウィジェットが横のレイアウトにまとまります。
  2. フォームの空いた場所をクリックしてウィンドウ自体を選択し、Lay Out Verticallyを押します。先ほどの横のまとまりとtodoListが縦に配置され、ウィンドウの大きさを変えるとウィジェットが追従して伸びます。

ウィジェットの間に空きが必要なら、ウィジェットボックスのHorizontal SpacerVertical Spacerをドラッグして入れます。スペーサーは画面には見えませんが余った空間を吸収するので、ボタンを右端に寄せるような配置を作れます。

作業が終わったらmain_window.uiとして保存します。

.uiファイルの正体 — XML #

保存されたファイルをエディタで開いてみると、特別なものではありません。ウィジェットのツリーとプロパティをそのまま収めたXML文書です。

main_window.uiの一部
<widget class="QLineEdit" name="todoInput"/>
<widget class="QPushButton" name="addButton">
 <property name="text">
  <string>追加</string>
 </property>
</widget>
<widget class="QListWidget" name="todoList"/>

核心は、このファイルには画面構造だけがあって動作がないという点です。何を押したら何が起きるかは、依然としてPythonコードの仕事です。画面とロジックがファイル単位で分離されるので、配置の修正はDesignerで、動作の修正はコードで、それぞれ進められます。

読み込み方式1 — pyside6-uicでコード生成 #

1つ目の方式は、.uiファイルをPythonコードに変換しておいてインポートするものです。

uiファイルをPythonコードに変換
pyside6-uic main_window.ui -o ui_main_window.py

生成されたui_main_window.pyの中にはUi_MainWindowクラスが入っていて、setupUiメソッドがDesignerで描いたウィジェットツリーをコードで再現します。使い方のパターンは次のとおりです。

setupUiパターン
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from ui_main_window import Ui_MainWindow


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.ui.addButton.clicked.connect(self.add_todo)

    def add_todo(self):
        text = self.ui.todoInput.text().strip()
        if text:
            self.ui.todoList.addItem(text)
            self.ui.todoInput.clear()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

self.ui.addButtonのように、Designerで付けたobjectNameがそのまま属性になります。生成されたファイルはインポートするだけの対象なので、直接編集しません。Designerでフォームを直したら、変換コマンドを再実行して更新します。

読み込み方式2 — QUiLoaderでランタイム読み込み #

2つ目の方式は、変換の手順なしに実行時へ.uiファイルを直接読むものです。

QUiLoaderパターン
import sys
from PySide6.QtWidgets import QApplication
from PySide6.QtUiTools import QUiLoader

app = QApplication(sys.argv)
loader = QUiLoader()
window = loader.load("main_window.ui")
window.addButton.clicked.connect(
    lambda: window.todoList.addItem(window.todoInput.text())
)
window.show()
app.exec()

ファイルを直して再実行すればすぐ反映されるので、手間が少なくて済みます。その代わり、windowがどのウィジェットを持つのかコードだけでは分からないため、エディタの自動補完と型チェックの助けを受けられません。またloadが完成したウィジェットを返す構造なので、ウィンドウクラスを継承で拡張するパターンとも相性がよくありません。

2つの方式の比較 #

項目pyside6-uic(コード生成)QUiLoader(ランタイム読み込み)
変換の手順必要(.uiを直すたびに再実行)なし
自動補完・型チェック生成コードを基準に動作非対応
クラス継承での拡張setupUiパターンで自然ぎこちない
配布生成された.pyだけ含めればよい.uiファイルを一緒に配布
向いている場面規模のあるアプリ、チーム開発素早い実験、小規模ツール

このシリーズの残りの回はsetupUiパターンを基準に進めます。自動補完が効く環境のほうが学習にも有利だからです。

ヒント
変換コマンドを毎回手で打つ代わりに、プロジェクトのスクリプトとして登録しておくと便利です。.uiファイルが複数あるなら、まとめて変換する1行のシェルスクリプトやMakefileターゲットを作っておくやり方が一般的です。

シグナル接続はコードで #

Designerにも、シグナルとスロットを画面上でつなぐ編集モードがあります。F4を押すと、ウィジェットをドラッグしてシグナルをつなぐ画面に切り替わります。ただしこのシリーズでは使いません。接続関係が.uiファイルの中に隠れると、どのボタンがどの動作を呼ぶのかをコード検索で見つけられなくなるためです。画面構造はDesignerが、動作と接続はコードが受け持つという境界を保つほうが、アプリが大きくなるほど有利です。

注記
Designerで作ったフォームでも、シグナルとスロットの動作原理は#3で整理したとおりです。clickedtextChangedのようなシグナルをコードのメソッドにconnectでつなぐという点は、画面をどう作ったかとは無関係に同じです。

まとめ #

今回の核心は3つです。

  • Qt DesignerはPySide6のインストールに含まれる画面編集ツールで、成果物の.uiファイルは画面構造だけを収めたXMLです。objectNameがコードからウィジェットを呼ぶ名前になります。
  • 読み込みは2方式です。pyside6-uicでコードを生成するsetupUiパターンが自動補完と拡張に有利で、QUiLoaderは変換なしで直接読む代わりに型情報を失います。
  • シグナル接続はDesignerではなくコードに置きます。画面と動作の境界がはっきりしていてこそ、規模が大きくなっても管理できます。

ここまでのToDoリストはQListWidgetに文字列を直接入れる方式でした。項目に期限と完了状態まで持たせるには、データと表示を分離する構造が必要です。次回の「PySide6でデスクトップアプリを作る #5 モデルとビュー — リスト・テーブルにデータをつなぐ」で、Qtのモデル/ビュー構造を扱います。

X