コンテンツにスキップ

第 9 章: 高度なトピック

これまでの章で、PySide6 アプリケーションを構築するための基本的な要素はすべて学びました。この章では、アプリケーションをより堅牢で、応答性が高く、ユーザーフレンドリーにするための高度なテクニックを 3 つ紹介します。

1. マルチスレッド処理:UI のフリーズを防ぐ

問題点:なぜ UI はフリーズするのか?

GUI アプリケーションは、単一のメインスレッド(GUI スレッド)でイベントループを回しています。もし、このメインスレッドで時間のかかる処理(例: 大きなファイルのダウンロード、複雑な計算、データベースへのクエリ)を実行すると、その処理が終わるまでイベントループがブロックされ、UI が一切の操作に反応しなくなります。これが「フリーズ」と呼ばれる現象です。

解決策:QThreadとワーカーオブジェクト

この問題を解決するには、時間のかかる処理をバックグラウンドの別スレッドで実行します。Qt で推奨される現代的な方法は、QThreadと「ワーカーオブジェクト」を組み合わせる手法です。

基本パターン

  1. 時間のかかる処理を、QObjectを継承した「ワーカー」クラスのメソッドとして実装します。
  2. ワーカークラスに、処理の進捗や結果を通知するためのシグナル(例: progress, finished)を定義します。
  3. メインウィンドウでQThreadとワーカーのインスタンスを作成します。
  4. ワーカーをmoveToThread()で新しいスレッドに移動させます。
  5. スレッドのstartedシグナルをワーカーの処理メソッドに接続します。
  6. ワーカーからのシグナルを、UI を更新するメインウィンドウのスロットに接続します。
  7. スレッドを開始します。

最も重要なルール: バックグラウンドスレッドから直接 UI ウィジェットを操作してはいけません。 UI の更新は、必ずシグナルを介してメインスレッドのスロット内で行います。

実践:プログレスバー付きの長時間タスク

# threading_example.py
import sys
import time
from PySide6.QtCore import QObject, QThread, Signal
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout,
    QPushButton, QProgressBar, QLabel
)

# 1. ワーカークラスの定義
class Worker(QObject):
    progress = Signal(int)    # 進捗を通知するシグナル
    finished = Signal(str)    # 終了を通知するシグナル

    def run(self):
        """時間のかかるタスク"""
        total_steps = 100
        for i in range(total_steps):
            time.sleep(0.05) # 処理をシミュレート
            self.progress.emit(i + 1)
        self.finished.emit("処理が完了しました。")

# 2. メインウィンドウ
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QThreadの例")
        # ... (UIのセットアップ)
        self.button = QPushButton("処理開始")
        self.progress_bar = QProgressBar()
        self.status_label = QLabel("待機中")
        # ... (レイアウト設定)

        self.button.clicked.connect(self.start_long_task)

    def start_long_task(self):
        self.button.setEnabled(False)
        self.status_label.setText("処理中...")

        # 3. スレッドとワーカーの準備
        self.thread = QThread()
        self.worker = Worker()
        self.worker.moveToThread(self.thread)

        # 4. シグナルとスロットの接続
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)

        self.worker.progress.connect(self.progress_bar.setValue)
        self.worker.finished.connect(self.on_task_finished)

        # 5. スレッドの開始
        self.thread.start()

    def on_task_finished(self, message):
        self.status_label.setText(message)
        self.button.setEnabled(True)

# ... (ウィンドウ表示のための定型コード)
# (省略したUIセットアップやメイン実行ブロックは他の章の例を参考にしてください)

2. QPainterによるカスタム描画

標準のウィジェットだけでは表現できない、独自のグラフや図形を描画したい場合があります。その際に使用するのがQPainterです。

QWidgetを継承したカスタムウィジェットを作成し、そのpaintEventメソッドをオーバーライドするのが基本です。paintEventは、ウィジェットが再描画される必要があるときに自動的に呼び出されます。

# custom_paint_widget.py
from PySide6.QtCore import Qt
from PySide6.QtGui import QPainter, QColor, QPen
from PySide6.QtWidgets import QWidget

class CircleWidget(QWidget):
    def paintEvent(self, event):
        # paintEvent内でQPainterをインスタンス化
        painter = QPainter(self)

        # アンチエイリアシングで滑らかに描画
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # ペンとブラシの設定
        pen = QPen(Qt.GlobalColor.black, 2)
        painter.setPen(pen)
        painter.setBrush(QColor(255, 0, 0)) # 赤いブラシ

        # ウィジェットの中央に円を描画
        center = self.rect().center()
        radius = min(self.width(), self.height()) / 2 - 5
        painter.drawEllipse(center, radius, radius)

このCircleWidgetを他のウィジェットと同様にレイアウトに追加すれば、赤い円が描画されたカスタムウィジェットとして利用できます。

3. 設定の永続化 (QSettings)

アプリケーションのウィンドウサイズや位置、ユーザーが設定した項目などを、次回の起動時にも復元したい場合があります。QSettingsは、このような設定をプラットフォーム非依存の方法で永続化するための便利なクラスです。

  • Windows: レジストリ
  • macOS: .plistファイル
  • Linux: .iniファイル

QSettingsは、これらの OS 標準の場所に設定を自動的に保存・読み込みします。

# settings_example.py
from PySide6.QtCore import QSettings

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        # ...
        self.load_settings() # 起動時に設定を読み込む

    def load_settings(self):
        # 会社名とアプリ名で設定を初期化
        settings = QSettings("MyCompany", "MyApp")
        geometry = settings.value("geometry")
        if geometry:
            self.restoreGeometry(geometry)

    def closeEvent(self, event):
        """ウィンドウが閉じられるときに自動的に呼ばれるメソッド"""
        settings = QSettings("MyCompany", "MyApp")
        # ウィンドウのジオメトリ(サイズと位置)を保存
        settings.setValue("geometry", self.saveGeometry())
        super().closeEvent(event)

__init__load_settingsを呼び、closeEvent(ウィンドウが閉じる直前に呼ばれるメソッド)をオーバーライドしてsaveGeometryで現在のウィンドウ情報を保存するだけで、次回の起動時にウィンドウが同じ場所・同じサイズで表示されるようになります。