これは、Qt Designer ファイルの変換、スクロール可能なレイアウトの作成、シグナルの接続、アプリケーションアイコンの変更、タブの並べ替え、時間のかかる処理を QThread に移す方法など、一般的な GUI タスクを扱うコンパクトな PyQt5 ガイドです。
.ui ファイルから .py ファイルを生成する
インターフェースを Qt Designer で設計している場合は、pyuic を使って .ui ファイルを Python に変換します。
python -m PyQt5.uic.pyuic -x [FILENAME].ui -o [FILENAME].py
-x オプションは、小さな実行可能テストブロックを追加します。生成されたファイルを別のモジュールからインポートするだけなら、-x は省略します。
python -m PyQt5.uic.pyuic [FILENAME].ui -o [FILENAME].py
有用なプロジェクト構成は次のようになります。
project/
├── main.py
├── ui_main.py # .ui から生成
├── workers.py # QThread/Worker コード
└── resources/
└── icon.png
生成された UI ファイルは、アプリケーションロジックから分離しておきます。.ui ファイルが変更された場合は、手動で編集するのではなく ui_main.py を再生成します。
ウィジェットをスクロール可能にする
大きなフォームや結果パネルをスクロール可能にするには、コンテンツ用ウィジェットを 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 PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QMainWindow
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("resources/icon.png"))
window = QMainWindow()
window.setWindowTitle("PyQt5 Application")
window.setWindowIcon(QIcon("resources/icon.png"))
window.show()
sys.exit(app.exec_())
簡単なスタイリングには、スタイルシートを使います。
window.setStyleSheet("""
QPushButton {
padding: 6px 10px;
}
QLineEdit {
padding: 4px;
}
""")
プロジェクトに専用のテーマファイルがない限り、スタイルシートは小さく保ちます。
シグナルを接続する
シグナルは .connect() でスロットに接続します。
self.button.clicked.connect(self.run_task)
スロットには任意の callable を使えます。
def run_task(self):
print("Button clicked")
引数を渡すには、lambda または functools.partial を使います。
self.button.clicked.connect(lambda: self.open_file("data.txt"))
ボタンを動的に再設定する場合は、新しいハンドラを接続する前に古いハンドラを切断します。
try:
self.button.clicked.disconnect()
except TypeError:
pass
self.button.clicked.connect(self.new_handler)
シグナルが接続されていない状態で disconnect() を呼ぶと TypeError が発生するため、接続状態が不確かな場合は捕捉します。
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 つのオブジェクトとして送出することもできます。
summary = pyqtSignal(tuple)
self.summary.emit((filename, count, elapsed_seconds))
長時間のタスクを QThread で実行する
遅い処理をボタンハンドラ内で直接実行してはいけません。イベントループがブロックされ、GUI が固まります。別の QThread で動作するワーカーオブジェクトに処理を移します。
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
class Worker(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(str)
failed = pyqtSignal(str)
@pyqtSlot()
def run(self):
try:
for i in range(101):
# Do part of the long-running task here.
self.progress.emit(i)
self.finished.emit("Task complete")
except Exception as exc:
self.failed.emit(str(exc))
def start_worker(self):
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.finished.connect(self.on_worker_finished)
self.worker.failed.connect(self.on_worker_failed)
self.worker.finished.connect(self.thread.quit)
self.worker.failed.connect(self.thread.quit)
self.thread.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
self.thread と self.worker への参照を保持してください。ローカル変数だけにしていると、タスクの実行中に Python がそれらをガベージコレクションしてしまう可能性があります。
タブの順序を変更する
QTabWidget では、removeTab() と insertTab() を使います。
index = self.tabs.indexOf(self.settings_tab)
widget = self.tabs.widget(index)
label = self.tabs.tabText(index)
icon = self.tabs.tabIcon(index)
self.tabs.removeTab(index)
self.tabs.insertTab(0, widget, icon, label)
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 コードは使い捨てにでき、ビジネスロジックはテストしやすくなります。メインウィンドウはインターフェースを調整し、ワーカークラスは遅い処理を担当し、ヘルパーモジュールには再利用可能な解析、前処理、計算関数を置くべきです。
