PyQt5 シンプルガイド

これは、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.threadself.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 コードは使い捨てにでき、ビジネスロジックはテストしやすくなります。メインウィンドウはインターフェースを調整し、ワーカークラスは遅い処理を担当し、ヘルパーモジュールには再利用可能な解析、前処理、計算関数を置くべきです。

Leave a Reply