メンテナンス層、2026-09-01 確認。 このガイドは、既存の PyQt5 アプリケーションと、Qt 5 を意図的に選んだチームのためのものです。2019 年の原チェックリストは、メンテナンス手順と分けて文末に全文保存しています。
この実践ガイドでは、PyQt5 の典型的な不具合を防ぐ小さな設計判断を扱います。具体的には、分離環境への導入、Qt Designer フォームのコード生成と実行時読み込みの選択、正しい QScrollArea、安定したリソースパス、特定のシグナル接続の追加と解除、GUI スレッドの応答性、QThread worker の後始末、そしてメタデータを失わないタブ移動です。
Table of Contents
2026 年の新規プロジェクトで PyQt5 を選ぶべきか
Riverbank は現在も PyQt5 と PyQt6 の両方を配布しています。PyQt5 は Qt 5、PyQt6 は Qt 6 のバインディングです。検証済みの Qt 5 アプリを保守する場合、または Qt 5 専用のコンポーネントに依存する場合は、PyQt5 を維持する理由があります。新規アプリでは、Qt 5 に固定する前に PyQt6 も評価し、選んだバインディングとバージョン範囲を依存関係ファイルに明記してください。グローバル環境に偶然入っているパッケージに依存してはいけません。
ライセンスはリリース要件であり、出荷前日に検討する細部ではありません。Riverbank は PyQt を GPL v3 と商用ライセンスのデュアルライセンスで提供しており、PyQt は LGPL ではありません。GPL と互換性のない条件でアプリを配布するなら、出荷前に Riverbank の公式ライセンス資料を読み、必要に応じて適切な専門家の助言を得てください。
再現可能な環境に PyQt5 を導入する
再作成可能な仮想環境を作り、そのインタープリターの pip でインストールします。
python -m venv .venv
# POSIX shells
source .venv/bin/activate
# Windows PowerShell
.venvScriptsActivate.ps1
python -m pip install --upgrade pip
python -m pip install PyQt5
アプリが実際に使っている Python、PyQt、Qt を確認します。
python -c "import sys; from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR; print(sys.executable); print('PyQt', PYQT_VERSION_STR, 'Qt', QT_VERSION_STR)"
Riverbank は、互換性のある wheel がない場合、pip が wheel からソース配布物へ切り替わることがあると説明しています。難解なコンパイラーや qmake のエラーが出ても、別の Qt パッケージを無作為に追加すべきだとは限りません。まず Python バージョン、プラットフォーム、pip バージョン、互換 wheel が選択されたかを確認します。
.ui ファイルから .py ファイルを生成する
Qt Designer はフォームを XML .ui ファイルとして保存します。PyQt5 が公式にサポートする方法は二つあります。pyuic5 で Python を生成するか、PyQt5.uic で .ui を実行時に読み込みます。コード生成は次のように行います。
pyuic5 -x main_window.ui -o ui_main_window.py
-x オプションは、小さな実行可能テストブロックを追加します。生成されたファイルを別のモジュールからインポートするだけなら、-x は省略します。
pyuic5 main_window.ui -o ui_main_window.py
有用なプロジェクト構成は次のようになります。
project/
├── main.py
├── ui_main.py # .ui から生成
├── workers.py # QThread/Worker コード
└── resources/
└── icon.png
生成された UI ファイルは、アプリケーションロジックから分離しておきます。.ui ファイルが変更された場合は、手動で編集するのではなく ui_main.py を再生成します。
生成工程を挟まずフォームを編集可能にしたい場合は、実行時読み込みが便利です。
from pathlib import Path
from PyQt5 import uic
from PyQt5.QtWidgets import QMainWindow
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
ui_file = Path(__file__).resolve().parent / "main_window.ui"
uic.loadUi(str(ui_file), self)
各フォームでどちらか一方のワークフローを選びます。生成モジュールは再現可能なビルド成果物であるべきで、実行時に読むフォームはインストールパッケージへ含める必要があります。Riverbank は、pyuic5 の生成コードがより古い PyQt5 で動く保証はないと注意しています。プロジェクトが固定したツールチェーンで生成してください。
ウィジェットをスクロール可能にする
大きなフォームや結果パネルをスクロール可能にするには、コンテンツ用ウィジェットを QScrollArea の中に配置します。
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QScrollArea, QLabel
content = QWidget()
layout = QVBoxLayout(content)
for i in range(100):
layout.addWidget(QLabel(f"Row {i}"))
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setWidget(content)
Qt Designer を使う場合は、QScrollArea を追加し、その中に子ウィジェットを置き、実際のレイアウトはその子ウィジェットに設定します。通常、スクロールエリア自体では widgetResizable を有効にしておくべきです。
外観を変更し、タスクバーアイコンを設定する
メインウィンドウに対して setWindowTitle() と setWindowIcon() を使います。ウィンドウを表示する前にアプリケーションアイコンを設定します。
import sys
from pathlib import Path
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QMainWindow
base_dir = Path(__file__).resolve().parent
icon = QIcon(str(base_dir / "resources" / "icon.png"))
app = QApplication(sys.argv)
app.setApplicationName("PyQt5 Application")
app.setWindowIcon(icon)
window = QMainWindow()
window.setWindowTitle("PyQt5 Application")
window.setWindowIcon(icon)
window.show()
sys.exit(app.exec_())
簡単なスタイリングには、スタイルシートを使います。
window.setStyleSheet("""
QPushButton {
padding: 6px 10px;
}
QLineEdit {
padding: 4px;
}
""")
プロジェクトに専用のテーマファイルがない限り、スタイルシートは小さく保ちます。リソースパスはプロセスのカレントディレクトリではなく、モジュールまたはパッケージを基準に解決します。アイコンや翻訳を埋め込むには、PyQt5 が .qrc ファイルと pyrcc5 で公開する Qt リソースシステムを使えます。タスクバーや Dock での最終表示は、プラットフォームとデスクトップ環境にも依存します。
シグナルを接続する
シグナルは .connect() でスロットに接続します。
self.button.clicked.connect(self.run_task)
スロットには任意の callable を使えます。
def run_task(self):
print("Button clicked")
引数を渡すには、lambda または functools.partial を使います。シグナル自身の引数も考慮してください。QAbstractButton.clicked は checked 状態を渡すことがあります。
self.button.clicked.connect(
lambda _checked=False: self.open_file("data.txt")
)
原則として接続は一度だけ行い、スロット内で状態により分岐させます。動的な接続交換が本当に必要な場合は、connect() が返す Connection を保持し、その接続だけを解除します。
self._open_connection = self.button.clicked.connect(
lambda _checked=False: self.open_file("data.txt")
)
# Later, before installing a replacement:
self.button.clicked.disconnect(self._open_connection)
self.button.clicked.connect(self.new_handler)
引数なしの disconnect() は、その bound signal に接続された全スロットを削除します。存在しない接続を解除すると例外になります。返された接続を保持すれば、他コンポーネントのハンドラを意図せず解除することを防げます。また、これが lambda を解除する文書化された方法です。
pyqtSignal を使う
カスタムシグナルはクラス属性として宣言します。
from PyQt5.QtCore import QObject, pyqtSignal
class Worker(QObject):
progress = pyqtSignal(int)
result = pyqtSignal(str)
failed = pyqtSignal(str)
シグナルは複数の値を送出できます。
class Worker(QObject):
finished = pyqtSignal(str, int)
型リストは次のパターンに従います。
pyqtSignal(type1, type2, ...)
複数の値については、グローバル変数に依存するのではなく、構造化された値を直接送出する方がよいでしょう。
self.finished.emit("done", 100)
タプルを 1 つの Python オブジェクトとして送出することもできます。
summary = pyqtSignal(tuple)
self.summary.emit((filename, count, elapsed_seconds))
長時間のタスクを QThread で実行する
遅い処理をボタンハンドラ内で直接実行してはいけません。イベントループがブロックされ、GUI が固まります。Qt 公式の worker-object パターンは QObject を別の QThread へ移し、結果をシグナルで GUI へ返します。worker からウィジェットを直接更新してはいけません。
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
class Worker(QObject):
progress = pyqtSignal(int)
result = pyqtSignal(str)
failed = pyqtSignal(str)
finished = pyqtSignal()
@pyqtSlot()
def run(self):
try:
for i in range(101):
if QThread.currentThread().isInterruptionRequested():
return
# Do part of the long-running task here.
self.progress.emit(i)
self.result.emit("Task complete")
except Exception as exc:
self.failed.emit(str(exc))
finally:
self.finished.emit()
def start_worker(self):
if getattr(self, "thread", None) is not None:
return
self.thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.worker.progress.connect(self.progress_bar.setValue)
self.worker.result.connect(self.on_worker_finished)
self.worker.failed.connect(self.on_worker_failed)
self.worker.finished.connect(self.thread.quit)
self.thread.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.finished.connect(self.clear_worker_references)
self.thread.start()
def cancel_worker(self):
if self.thread is not None:
self.thread.requestInterruption()
def clear_worker_references(self):
self.worker = None
self.thread = None
finished まで両オブジェクトへの参照を保持します。ローカル変数だけの Python wrapper は、処理中に回収される可能性があります。キャンセルは協調的でなければなりません。requestInterruption() はフラグを立てるだけなので、worker が安全な境界でそれを確認します。成功、失敗、キャンセルのどの経路でも finished を送出し、イベントループを終了し、Qt 所有オブジェクトの削除を予約するようにします。
QThread は応答性を改善し、ブロッキング I/O や、Python のグローバルインタープリターロックを解放するネイティブ処理に適します。通常の GIL 有効 CPython では、純 Python の CPU バウンド処理が自動的に並列化されるわけではありません。その場合はまず計測し、プロセス型 worker を検討します。free-threaded CPython ビルドも存在しますが、それは別のデプロイ選択であり、拡張機能の互換性を確認する必要があります。
タブの順序を変更する
ユーザーがタブをドラッグすべき場合は、組み込みの動作を有効にします。
self.tabs.setMovable(True)
プログラムが制御して移動するなら、インデックスを検証し、removeTab() と insertTab() の前後でタブのメタデータを保持します。
index = self.tabs.indexOf(self.settings_tab)
if index < 0:
raise ValueError("settings_tab is not in the tab widget")
widget = self.tabs.widget(index)
label = self.tabs.tabText(index)
icon = self.tabs.tabIcon(index)
tooltip = self.tabs.tabToolTip(index)
enabled = self.tabs.isTabEnabled(index)
self.tabs.removeTab(index)
self.tabs.insertTab(0, widget, icon, label)
self.tabs.setTabToolTip(0, tooltip)
self.tabs.setTabEnabled(0, enabled)
self.tabs.setCurrentWidget(widget)
タブを Qt Designer で作成している場合は、そこで並べ替える方が簡単なことがよくあります。順序がユーザー設定や実行時の状態に依存する場合は、コードを使います。
前処理と制御ロジックをモジュール化する
保守しやすい PyQt プロジェクトでは、UI セットアップ、前処理、制御ロジックを分離します。
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.connect_signals()
def connect_signals(self):
self.ui.runButton.clicked.connect(self.run_preprocessing)
def run_preprocessing(self):
options = self.collect_options()
self.start_worker(options)
def collect_options(self):
return {
"input": self.ui.inputLineEdit.text(),
"enabled": self.ui.enableCheckBox.isChecked(),
}
これにより、生成 UI コードはいつでも再生成でき、ビジネスロジックはテストしやすくなります。メインウィンドウは画面を調整し、worker クラスは時間のかかるオーケストレーションを担当し、通常のヘルパーモジュールには再利用可能な解析、前処理、計算関数を置きます。
小規模プロジェクトでも、その境界をディレクトリに表せます。
project/
├── pyproject.toml
├── src/app/
│ ├── main.py
│ ├── window.py
│ ├── ui_main_window.py # generated; do not hand-edit
│ ├── workers.py
│ ├── services.py # no widget access
│ └── resources/
└── tests/
不具合チェックリスト
- ウィンドウが固まる: スロットが GUI スレッドで長い処理を続けています。
QThread: Destroyed while thread is still running: コントローラーが thread/worker を保持していないか、アプリ終了時に停止と待機を行っていません。- worker がウィジェットに触る: データをシグナルで送り、GUI スレッドのスロットで画面を更新します。
- スクロール領域が空白: 一つの子ウィジェットにレイアウトを設定してから、その子を
setWidget()へ渡します。 - プロジェクトディレクトリから起動したときだけアイコンが出る:
__file__から解決するか、Qt リソースシステムを使います。 - 1 回のクリックで 2 回実行される: 同じシグナルが重複接続されています。接続初期化を一か所に集めます。
disconnect()が例外になる: 接続状態を推測せず、正確な callable または返されたConnectionを追跡します。- タブから tooltip や有効状態が消える: remove/insert 前後で全メタデータを保持するか、
setMovable(True)でユーザーに移動させます。
一次資料となる公式文書
- Riverbank:PyQt の概要、バインディング世代、ライセンス
- Riverbank:PyQt5 のインストール
- Riverbank:Qt Designer と `pyuic5`
- Riverbank:PyQt5 のシグナル、スロット、接続オブジェクト、`pyqtSignal`
- Riverbank:PyQt5 リソースシステム
- Qt:`QThread` と worker-object パターン
- Qt:`QScrollArea`
- Qt:`QTabWidget`
- Python:`venv` による仮想環境の作成
- Python:GIL とスレッド性能の注意点
---
2019 年の原アーカイブ(英語原文を保存)
以下は、2019-04-15 公開、2019-04-20 最終更新の WordPress 原エクスポートにある、目に見える語句と順序の全文です。リポジトリの形式チェックのため、目に見えない行末空白だけを正規化しています。
Make it scrollable
Generating .py file from .ui
python -m PyQt5.uic.pyuic -x [FILENAME].ui -o [FILENAME].py
Modularized
Change appearance & setup taskbar icon
Signal connection
Parallel processing
pyqtSignal, QThread, Worker
pyqtSignal[type1, type2, …]
using global variable, tuple
using disconnect to clear outdated signal
button.clicked.disconnect()
Change the order of tab
modularized the pre-processing and control part
