TkinterやPyQt5ではなくPySide6を選ぶ理由
自分の自動化プロジェクトはもともと200行だけ — ターミナルで動く小さなスクリプトで十分だった。だが6ヶ月後、2000行に膨れ上がり、クライアントはGUIを求め、コマンドを打つ代わりにボタンをクリックしたがった。最初にTkinterを試したが、速いものの見た目がWindows 98のようだった。それからPySide6に乗り換え、二度と振り返らなかった。
PySide6はPython向けQt 6の公式バインディングで、The Qt CompanyがLGPLライセンスで開発・提供しています — つまり商用プロジェクトでも無料で使えます。PyQt5(PyQt6)と比べて、PySide6はライセンスが柔軟でAPIは同等です。Tkinterと比べると、モダンなウィジェット、CSS風スタイリングのサポート、そして最も重要なQt Designer — 直感的なドラッグ&ドロップUIデザインツール — を備えています。
6ヶ月の実使用を経て、最初からPySide6アプリを正しく構成する方法について学んだことをまとめます。
環境のセットアップ
他のプロジェクトとの競合を避けるためにvirtualenvから始めます:
python -m venv venv
source venv/bin/activate # Linux/macOS
# または: venv\Scripts\activate # Windows
pip install PySide6
上記のコマンドはQt Designer、Qt Quick、および必要なモジュールを含むQt 6バインディング全体をインストールします。サイズは約150MB — 最初は少し時間がかかりますが、一度だけです。
インストールの成功を確認:
python -c "import PySide6; print(PySide6.__version__)"
ターミナルからQt Designerを直接起動:
pyside6-designer
Qt Designerでインターフェースをデザインする
Qt Designerこそ、PySide6が他のPython GUIライブラリよりも際立っている点だと感じます。手動でレイアウトコードを書く代わりに、ウィジェットをドラッグ&ドロップし、プロパティを設定して、.uiファイル(実質的にXML)として保存します。Designerに慣れるのに約2日かかりましたが、その後は生産性が格段に上がりました。
最初の.uiファイルを作成する
DesignerでFile → New → Main Windowを選択します。左のパネルからキャンバスにウィジェットをドラッグします:
- QLabel — 静的テキストを表示
- QLineEdit — 入力フィールド
- QPushButton — ボタン
- QTextEdit — 複数行テキストエリア
Propertiesパネルで各ウィジェットのobjectNameを設定してください — これが後でPythonコード内で使用する名前です。意味のある名前を付けましょう:btn_submit、input_username、label_status。
ファイルをmain_window.uiとして保存します。
.uiをPythonに変換する
コード内で.uiファイルを読み込む方法は2つあります:
方法1:ランタイム時に直接ロード(開発時に便利):
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtUiTools import QUiLoader
from PySide6.QtCore import QFile
import sys
loader = QUiLoader()
file = QFile("main_window.ui")
file.open(QFile.ReadOnly)
window = loader.load(file)
file.close()
window.show()
方法2:Pythonクラスに変換(本番環境で推奨):
pyside6-uic main_window.ui -o ui_main_window.py
生成されたui_main_window.pyファイルにはインポートして継承できるUi_MainWindowクラスが含まれています。IDEがウィジェット名を自動補完でき、タイプミスによるバグを減らせるため、方法2を使います。
本番対応のコード構成
6ヶ月で得た最も貴重な教訓は:すべてのロジックを一つのクラスに詰め込まないことです。プロジェクトが200行から2000行に成長すると、フラットな構成は悪夢になります。シンプル化したModel-View-Controllerパターンで整理しています:
myapp/
├── main.py # エントリーポイント
├── ui/
│ ├── main_window.ui # Qt Designerファイル
│ └── ui_main_window.py # 自動生成ファイル、手動で編集しないこと
├── views/
│ └── main_window.py # コントローラー:UIとロジックを接続
└── core/
└── processor.py # ビジネスロジック(純粋なPython)
views/main_window.py — すべてをつなぐファイル
from PySide6.QtWidgets import QMainWindow, QMessageBox
from PySide6.QtCore import Qt, QThread, Signal
from ui.ui_main_window import Ui_MainWindow
from core.processor import DataProcessor
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.processor = DataProcessor()
self._connect_signals()
def _connect_signals(self):
# シグナルとスロットを接続 — これがQtの核心
self.ui.btn_submit.clicked.connect(self._on_submit_clicked)
self.ui.input_username.returnPressed.connect(self._on_submit_clicked)
def _on_submit_clicked(self):
username = self.ui.input_username.text().strip()
if not username:
QMessageBox.warning(self, "エラー", "ユーザー名を入力してください")
return
result = self.processor.process(username)
self.ui.label_status.setText(f"結果: {result}")
QThreadで重い処理を扱う
Qtを使い始めたときの最大の問題:スロット内で直接重い処理(API呼び出し、大きなファイルの読み込み)を実行するとUIが完全にフリーズします。解決策はQThreadです:
from PySide6.QtCore import QThread, Signal
class WorkerThread(QThread):
# 結果をメインスレッドに送信するシグナル
result_ready = Signal(str)
error_occurred = Signal(str)
def __init__(self, username: str):
super().__init__()
self.username = username
def run(self):
# 別スレッドで実行 — ここでUIを更新してはいけない
try:
import time
time.sleep(2) # 重い処理をシミュレート
result = f"Processed: {self.username.upper()}"
self.result_ready.emit(result)
except Exception as e:
self.error_occurred.emit(str(e))
# MainWindow._on_submit_clicked() 内:
def _on_submit_clicked(self):
username = self.ui.input_username.text().strip()
self.ui.btn_submit.setEnabled(False) # ダブルクリックを防ぐために無効化
self.ui.label_status.setText("処理中...")
self.worker = WorkerThread(username)
self.worker.result_ready.connect(self._on_result_ready)
self.worker.error_occurred.connect(self._on_error)
self.worker.finished.connect(lambda: self.ui.btn_submit.setEnabled(True))
self.worker.start()
def _on_result_ready(self, result: str):
self.ui.label_status.setText(result)
def _on_error(self, error: str):
QMessageBox.critical(self, "エラー", error)
重要な注意点:ワーカースレッドへの参照を必ず保持してください(self.worker) — ローカル変数にすると、Pythonのガベージコレクターがスレッドの実行完了前に破棄してしまいます。
QSSでスタイリング — QtのCSS
QtはQSSと呼ばれるCSSに似たスタイルシートをサポートしています。編集しやすいように、別ファイルから読み込むのが一般的です:
# main.py 内
app = QApplication(sys.argv)
with open("style.qss", "r") as f:
app.setStyleSheet(f.read())
/* style.qss */
QPushButton {
background-color: #2563eb;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
}
QPushButton:hover {
background-color: #1d4ed8;
}
QPushButton:disabled {
background-color: #9ca3af;
}
QLineEdit {
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 6px 10px;
font-size: 14px;
}
アプリケーションのテストとパッケージング
アプリケーションの実行
# main.py
import sys
from PySide6.QtWidgets import QApplication
from views.main_window import MainWindow
def main():
app = QApplication(sys.argv)
app.setApplicationName("MyApp")
app.setApplicationVersion("1.0.0")
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
.exeまたはバイナリにパッケージング
pyinstallerを使ってパッケージングします:
pip install pyinstaller
# 単一ファイルにパッケージング
pyinstaller --onefile --windowed \
--add-data "ui/*.ui:ui" \
--add-data "style.qss:." \
main.py
出力ファイルはdist/ディレクトリに生成されます。Windowsでは.exe、Linuxでは拡張子なしのバイナリ、macOSでは.appが出力されます。
よくある問題
- UIフリーズ:メインスレッドで重い処理を実行している → 上記で説明したQThreadを使用
- ウィジェットが更新されない:シグナルのemitを忘れているか、サブスレッドからUIを更新している → シグナル経由でメインスレッドからのみUIを更新
- .uiファイルがパッケージング後に見つからない:pyinstallerコマンドに
--add-dataを追加するか、Pythonクラスに変換する(上記の方法2)でこの問題を完全に回避 - メモリリーク:parentなしでウィジェットを作成している → Qtが自動でクリーンアップしない。子ウィジェットを作成する際は必ず
parent=selfを渡す
まとめ
PySide6を6ヶ月使って気づいたことをまとめると:最大の強みはSignal/Slotシステムです — このメカニズムを理解すれば、すべてのUI操作が明確になりテスト可能になります。Qt Designerは手動でコードを書くよりもレイアウトデザインの時間を大幅に節約してくれます。
クロスプラットフォームで動作し、スタイリングが美しく、フリーズなしに重い処理を扱えるGUIがプロジェクトに必要なら — PySide6は堅実な選択肢です。最初からUI/ロジック/コアを分離するパターンでコードを構成すれば、プロジェクトが大きくなってもすべてをリファクタリングする必要がなくなります。

