PySideで始めるデスクトップアプリ開発:特徴と使い方


PySideで始めるデスクトップアプリ開発:特徴と使い方

はじめに:デスクトップアプリ開発とPySide

現代のソフトウェア開発において、Webアプリケーションやモバイルアプリケーションが主流となりつつありますが、デスクトップアプリケーションには依然として重要な役割があります。特定の業務処理、高度なグラフィック処理、ローカルリソースへのアクセス、オフラインでの利用など、デスクトップアプリケーションでなければ実現が難しい、あるいは最適なユーザー体験を提供できない場面は数多く存在します。

Pythonは、その書きやすさ、豊富なライブラリ、そして多様な用途から、多くの開発者に愛されています。Pythonでデスクトップアプリケーションを開発するための選択肢はいくつか存在しますが、その中でも特に強力で広く利用されているのが、QtフレームワークのPythonバインディングです。そして、そのQt for Pythonの公式プロジェクトとして開発されているのが PySide です。

この記事では、PySide(具体的にはQt 6向けのPySide6)を用いて、本格的なデスクトップアプリケーションを開発するための第一歩を踏み出すための詳細な情報を提供します。PySide/Qtの持つ強力な機能、開発環境の構築方法、基本的なウィジェットの使い方、UI設計ツールであるQt Designerの活用、そして実践的なトピックまでを網羅的に解説し、読者の皆様がPythonでデスクトップアプリケーションを開発する力を身につけられるよう支援します。

PySide/Qtの特徴:なぜQtを選ぶのか

PySideがベースとしているQtフレームワークは、C++で記述された非常に成熟したクロスプラットフォームアプリケーション開発フレームワークです。このQtの機能をPythonから利用できるようにしたのがPySideであり、Qtの持つ多くの強力な特徴をPythonの世界にもたらしています。

Qtを選択する主な理由は以下の通りです。

  1. クロスプラットフォーム性:
    Qtの最大の強みの一つは、その優れたクロスプラットフォーム性です。Windows、macOS、Linuxといった主要なデスクトップOSはもちろん、組み込みシステム、モバイル(Android/iOS、限定的)、さらにはWebAssembly(Webブラウザ上で動作)まで、幅広いプラットフォームに対応しています。一度書いたコードを大きな変更なく様々な環境で実行できるため、開発効率が飛躍的に向上します。PySideもこの特性を受け継いでおり、Pythonコードを一つのソースで複数のOSに対応させることが可能です。

  2. 豊富なウィジェット(GUIコンポーネント):
    Qtには、ボタン、ラベル、テキスト入力フィールド、リスト、テーブル、ツリー、グラフなど、標準的なGUIアプリケーションに必要なあらゆる種類のウィジェットが用意されています。これらのウィジェットは、各プラットフォームのネイティブなルック&フィールに近い形で描画されるため、ユーザーに違和感のない操作感を提供できます。また、高度なウィジェットや、既存のウィジェットをカスタマイズ・拡張するための強力な機能も備わっています。

  3. シグナル&スロット機構:
    Qtの核となるイベント処理メカニズムが「シグナル&スロット」です。これは、あるオブジェクト(ウィジェットなど)で発生したイベント(シグナル、例:ボタンがクリックされた)を、別のオブジェクトの特定の関数やメソッド(スロット、例:クリック時に実行する処理)に接続(connect)する仕組みです。このモデルは、オブジェクト間の疎結合を促進し、イベントドリブンなプログラミングを非常に直感的かつ安全に行えるように設計されています。PySideでも、Pythonのメソッドや関数をシグナルに接続することが容易に行えます。

  4. Qt Designerによる直感的なUI設計:
    Qtには「Qt Designer」というGUIレイアウトツールが付属しています。これを使えば、コードを書くことなく、ドラッグ&ドロップでウィジェットを配置し、ウィンドウのレイアウトを設計できます。デザインしたUIは.uiファイルとして保存され、これをPythonコードから読み込んで利用することが可能です。これにより、デザインとロジックを分離した開発が可能となり、UIの変更が容易になります。

  5. 国際化対応 (i18n) とローカライゼーション (l10n):
    Qtは、アプリケーションの国際化とローカライゼーションを強力にサポートしています。文字列のリソース管理、翻訳ファイルの作成・利用など、多言語対応アプリケーションの開発に必要な機能が揃っています。

  6. Qt Quick (QML) による宣言的UI開発:
    従来のウィジェットシステム(Qt Widgets)に加えて、QtはQt Quickという別のUI開発アプローチも提供しています。Qt Quickでは、QMLという宣言的な言語を用いて、アニメーションや効果を含むリッチなUIを効率的に記述できます。PySideはこのQt Quickもサポートしており、用途に応じて適切なUI技術を選択できます。この記事では主にQt Widgetsに焦点を当てますが、PySideがQMLも扱えることを知っておくことは重要です。

  7. 豊富なモジュール:
    QtはGUI機能だけでなく、ネットワーク通信、データベースアクセス、XML/JSON処理、マルチメディア、OpenGLによるグラフィックスなど、アプリケーション開発に必要な様々な機能を提供するモジュールを備えています。これらのモジュールをPySide経由で利用することで、Pythonの標準ライブラリやサードパーティライブラリと組み合わせて、非常に強力なアプリケーションを開発できます。

  8. LGPLライセンス:
    PySideは、Qt for Pythonプロジェクトとして開発されており、LGPL (Lesser General Public License) バージョン 3 の下で提供されています。これは、Qt自体がLGPLまたは商用ライセンスで提供されているためです。LGPLライセンスは、オープンソースのライセンスですが、一般的なGPLとは異なり、アプリケーション全体をオープンソースにする必要はありません。Qtライブラリを動的にリンク(多くの場合はこの形式になります)して使用する場合、ご自身のアプリケーションのソースコードを公開せずに、商用ソフトウェアとして配布することが可能です。これは、商用目的でデスクトップアプリケーションを開発する際に非常に重要なメリットとなります。

  9. PyQtとの比較:
    PythonでQtを利用するためのバインディングとしては、PySideの他に「PyQt」があります。PyQtはRiverbank Computing社によって開発されており、歴史的にはPySideよりも先行していました。PyQtはGPLまたは商用ライセンスで提供されています。以前はライセンス体系がPySideとPyQtの大きな違いでしたが、PySideがQt Companyによる公式開発となりLGPLで提供されるようになったことで、特に商用利用を検討している開発者にとってPySideが有力な選択肢となりました。機能的には両者に大きな差はありませんが、PySideはQtの公式プロジェクトであるという安心感や、Qtの新機能への対応が早い傾向があるといった点が特徴として挙げられます。(本記事ではPySide6に焦点を当てます。)

これらの特徴から、PySide/Qtは、プロフェッショナルなデスクトップアプリケーションをPythonで開発するための、非常に強力で実用的なフレームワークと言えます。

開発環境の構築

PySideを使った開発を始めるには、まずPythonとPySide6ライブラリをインストールする必要があります。推奨される方法で環境を構築しましょう。

  1. Pythonのインストール:
    PySide6はPython 3.6以降をサポートしています。公式ウェブサイト (python.org) からお使いのOSに合ったPythonインストーラーをダウンロードし、インストールしてください。インストール時には、「Add Python to PATH」のオプションをチェックすることを強く推奨します。

  2. 仮想環境の利用(推奨):
    Pythonプロジェクトごとに依存関係を分離するために、仮想環境を利用することを強く推奨します。これにより、プロジェクト間でライブラリのバージョン衝突を防ぐことができます。Python 3.3以降にはvenvモジュールが標準で含まれています。

    ターミナル(コマンドプロンプトやPowerShellなど)を開き、プロジェクトディレクトリに移動して以下のコマンドを実行します。

    • Windows:
      bash
      python -m venv venv
      venv\Scripts\activate
    • macOS/Linux:
      bash
      python3 -m venv venv
      source venv/bin/activate

      これにより、venvという名前の仮想環境が作成され、アクティベートされます。仮想環境がアクティベートされている間は、インストールされるパッケージはその環境内に限定されます。
  3. PySide6のインストール:
    仮想環境をアクティベートした状態で、以下のpipコマンドを実行してPySide6をインストールします。

    bash
    pip install PySide6

    このコマンドにより、PySide6本体だけでなく、Qtの各モジュール(Widgets, QtCore, QtGuiなど)やツール(Qt Designer, pyside6-uicなど)も一緒にインストールされます。インストールには少し時間がかかる場合があります。

  4. Qt Designerの起動:
    PySide6をインストールすると、Qt Designerも一緒にインストールされます。インストール場所はOSやPythonのバージョンによって異なりますが、仮想環境内のsite-packages/PySide6/designerディレクトリ以下にdesignerまたはdesigner.exeという実行ファイルとして配置されていることが多いです。

    簡単に起動するために、PySide6にはpyside6-designerというコマンドも用意されています。仮想環境をアクティベートしたターミナルで以下のコマンドを実行すると、Qt Designerが起動します。

    bash
    pyside6-designer

  5. IDEの準備:
    VS CodeやPyCharmなどの統合開発環境(IDE)を使用すると、コード補完、デバッグ、シンタックスハイライトなどの機能により開発効率が向上します。使用するIDEで、先ほど作成した仮想環境のPythonインタープリターを設定してください。これにより、PySide6ライブラリが正しく認識され、コード補完などが機能するようになります。

これで、PySideを使ったデスクトップアプリケーション開発を行うための環境構築は完了です。

PySideの基本:最小のアプリケーションから始める

PySideを使った最初のアプリケーションは、非常にシンプルなウィンドウを表示するだけのものです。ここからPySideアプリケーションの基本的な構造を学びましょう。

“`python
import sys
from PySide6.QtWidgets import QApplication, QWidget

1. QApplication インスタンスの作成

QApplicationはGUIアプリケーション全体を管理する唯一のインスタンスです。

コマンドライン引数を渡す必要があります。

app = QApplication(sys.argv)

2. メインウィンドウ(またはウィジェット)の作成

QWidgetは最も基本的なGUI要素です。ここでは単純な空のウィンドウとして使います。

window = QWidget()

ウィンドウのタイトルを設定

window.setWindowTitle(“初めてのPySideアプリ”)

ウィンドウのサイズを設定 (幅, 高さ)

window.setGeometry(100, 100, 280, 80) # (x座標, y座標, 幅, 高さ)

3. ウィンドウを表示

window.show()

4. アプリケーションの実行(メインループの開始)

app.exec() または app.exec_() はイベントループを開始し、

アプリケーションが終了するまでブロックします。

ウィンドウが表示され、ユーザー操作(クリック、キー入力など)に応答可能になります。

アプリケーションが終了すると、exec() は終了コードを返します。

sys.exit(app.exec())
“`

コードの解説:

  1. import sysfrom PySide6.QtWidgets import ...:
    必要なモジュールをインポートしています。sysモジュールはコマンドライン引数を扱うため、QApplicationのインスタンス化に必要です。PySide6.QtWidgetsモジュールには、GUIを構築するための基本的なウィジェットクラスが含まれています。ここではQApplicationQWidgetをインポートしています。

  2. app = QApplication(sys.argv):
    QApplicationクラスのインスタンスを作成します。これはPySide/Qt GUIアプリケーションには必ず必要なオブジェクトで、アプリケーション全体の設定、イベントループの管理、ウィジェットの管理などを担当します。sys.argvを引数として渡すのが慣例です。アプリケーションは必ず一つだけQApplicationインスタンスを持つ必要があります。

  3. window = QWidget():
    GUIウィンドウとして表示するためのQWidgetインスタンスを作成します。QWidgetは、ボタンやラベルのような個々の部品であると同時に、それらを配置するためのコンテナとしても使われます。ここでは、他のウィジェットを含まない単純なトップレベルウィンドウとして使用しています。

  4. window.setWindowTitle("初めてのPySideアプリ"):
    ウィンドウのタイトルバーに表示されるテキストを設定します。

  5. window.setGeometry(100, 100, 280, 80):
    ウィンドウの初期位置とサイズを設定します。引数は左上隅のX座標、Y座標、ウィンドウの幅、高さです。

  6. window.show():
    作成したウィンドウを画面に表示します。デフォルトでは、ウィジェットは作成されただけでは見えません。

  7. sys.exit(app.exec()):
    app.exec()メソッド(またはapp.exec_() – Pythonのキーワードとの衝突を避けるための別名、機能は同じ)は、Qtのイベントループを開始します。イベントループは、ユーザーからの入力(マウスクリック、キーボード入力など)やシステムからのイベント(ウィンドウのリサイズ、再描画要求など)を待ち受け、それに対応する処理(シグナルとスロットの接続によって定義)を実行します。このメソッドはアプリケーションが終了するまで制御を返しません。アプリケーションが終了すると、exec()は終了コードを返します。慣例として、この終了コードをsys.exit()に渡してPythonスクリプト全体の終了ステータスとします。

この短いコードで、PySideを使った基本的なGUIアプリケーションの起動、ウィンドウ表示、そしてイベント処理の開始という一連の流れを理解できます。

ウィジェットの配置とレイアウト

実際のアプリケーションでは、ウィンドウ内に複数のウィジェット(ボタン、テキストボックスなど)を配置する必要があります。PySideでは、ウィジェットの配置には「レイアウトマネージャー」を使用するのが一般的です。レイアウトマネージャーを使うことで、ウィンドウのリサイズに合わせてウィジェットの位置やサイズが自動的に調整される、柔軟なUIを作成できます。

PySide/Qtでよく使われる主要なレイアウトクラスは以下の通りです。

  • QVBoxLayout: ウィジェットを垂直方向に並べます。
  • QHBoxLayout: ウィジェットを水平方向に並べます。
  • QGridLayout: ウィジェットをグリッド(格子状)に配置します。
  • QFormLayout: ラベルと入力ウィジェットのペアを配置するのに適しています(例:設定画面やデータ入力フォーム)。

レイアウトを使用するには、以下の手順を踏みます。

  1. レイアウトインスタンスを作成します。
  2. レイアウトにウィジェットを追加します(addWidget()メソッド)。
  3. ウィジェットやレイアウトの間にスペースを追加することも可能です(addSpacing(), addStretch())。
  4. レイアウトを、親となるウィジェット(通常はウィンドウ全体を表すウィジェット、またはコンテナウィジェット)に設定します(setLayout()メソッド)。

以下のコード例では、QVBoxLayoutQHBoxLayoutを組み合わせて、いくつかのウィジェットを配置してみます。

“`python
import sys
from PySide6.QtWidgets import (QApplication, QWidget, QLabel, QPushButton,
QVBoxLayout, QHBoxLayout, QLineEdit)

class MyWindow(QWidget):
def init(self):
super().init()

    self.setWindowTitle("レイアウトを使ったアプリ")
    # ウィンドウの初期サイズ設定
    self.setGeometry(100, 100, 400, 200)

    # メインレイアウトとして垂直ボックスレイアウトを作成
    main_layout = QVBoxLayout()

    # ラベルを作成し、中央揃えに設定
    self.label = QLabel("ここにテキストが表示されます")
    # Qt.AlignHCenterはPySide6.QtCore.Qtクラスの一部
    from PySide6.QtCore import Qt
    self.label.setAlignment(Qt.AlignmentFlag.AlignHCenter) # Qt6ではAlignmentFlag列挙型を使用

    # 水平ボックスレイアウトを作成し、LineEditとボタンを配置
    input_layout = QHBoxLayout()

    self.textbox = QLineEdit()
    self.button = QPushButton("テキスト設定")

    # 水平レイアウトにウィジェットを追加
    input_layout.addWidget(self.textbox)
    input_layout.addWidget(self.button)
    # addStretch() を使うと、追加されたウィジェットの後ろに可変長のスペースを追加します。
    # これにより、ウィジェットが左詰めで配置され、残りのスペースが埋められます。
    # input_layout.addStretch()

    # メインレイアウトにラベルを追加
    main_layout.addWidget(self.label)

    # メインレイアウトに水平レイアウトを追加
    # レイアウトの中に別のレイアウトを追加することも可能です
    main_layout.addLayout(input_layout)

    # メインレイアウトに可変長のスペースを追加して、ウィジェットを上部に詰める
    main_layout.addStretch()

    # ウィンドウのレイアウトを設定
    self.setLayout(main_layout)

    # シグナルとスロットの接続(後述)
    self.button.clicked.connect(self.set_label_text)

# ボタンがクリックされたときに実行されるスロット
def set_label_text(self):
    text = self.textbox.text() # LineEditからテキストを取得
    self.label.setText(text)   # ラベルにテキストを設定

アプリケーションのエントリーポイント

if name == “main“:
app = QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec())
“`

コードの解説:

  1. クラス化:
    アプリケーションのメインウィンドウをQWidgetを継承したクラスとして定義するのが、より一般的な開発スタイルです。コンストラクタ__init__内でUIの構築を行います。

  2. レイアウトの作成と追加:
    QVBoxLayout()で垂直レイアウト、QHBoxLayout()で水平レイアウトのインスタンスを作成しています。
    main_layout.addWidget(self.label)のように、レイアウトのaddWidget()メソッドを使ってウィジェットをレイアウトに追加します。
    main_layout.addLayout(input_layout)のように、addLayout()メソッドを使うと、レイアウトの中に別のレイアウトをネストできます。これにより、複雑な配置構造を表現できます。
    addStretch()は、利用可能なスペースを埋めるための伸縮自在な空間を追加します。これを使うことで、ウィジェットをレイアウトの端に寄せたり、中央に配置したりできます。

  3. self.setLayout(main_layout):
    作成したレイアウトインスタンスを、親となるウィジェット(この場合はMyWindowインスタンス自身)に設定します。これにより、ウィンドウがリサイズされた際に、このレイアウトマネージャーが子ウィジェットの配置を自動的に調整するようになります。

この例では、垂直方向のメインレイアウトの中に、水平方向の入力レイアウトとラベルを配置しています。入力レイアウトは、テキストボックスとボタンを水平に並べています。ウィンドウのサイズを変更すると、ウィジェットがレイアウトのルールに従って自動的に配置されることを確認できます。

シグナルとスロット:イベント処理の仕組み

PySide/Qtのイベント処理の根幹をなすのがシグナルとスロット機構です。これは、特定のイベント(例:ボタンのクリック、テキストの変更、ウィンドウのリサイズなど)が発生した際に、定義された関数やメソッド(スロット)を実行するための仕組みです。

  • シグナル (Signal):
    特定のイベントが発生したことを通知するものです。ウィジェットは様々な組み込みシグナルを持っています(例: QPushButtonclickedシグナル、QLineEdittextChangedシグナル)。

  • スロット (Slot):
    シグナルが発生したときに実行される関数またはメソッドです。通常は、PySide/Qtオブジェクトのメソッドや、Pythonの通常の関数、ラムダ関数などをスロットとして使用します。

  • 接続 (Connect):
    シグナルとスロットを関連付ける操作です。シグナルのconnect()メソッドを使って行います。

シグナルとスロットを接続することで、UI上でのユーザーのアクションやシステムイベントに対応した処理を記述できます。上記のレイアウトの例では、既にボタンのclickedシグナルと、MyWindowクラスのset_label_textメソッド(スロットとして機能)を接続しています。

“`python

レイアウトの例から抜粋

シグナルとスロットの接続

self.button.clicked.connect(self.set_label_text)

ボタンがクリックされたときに実行されるスロット(MyWindowクラスのメソッド)

def set_label_text(self):
text = self.textbox.text()
self.label.setText(text)
“`

このコードでは、self.button(QPushButtonインスタンス)のclickedシグナルが発せられたときに、self.set_label_textメソッドが呼び出されるように接続しています。ユーザーがボタンをクリックすると、clickedシグナルが発せられ、それに応じてset_label_textメソッドが実行され、テキストボックスの内容がラベルに反映される、という流れになります。

connect()メソッドの引数には、呼び出したいスロット(関数やメソッド)を指定します。PySideでは、Pythonの通常の関数やメソッドをそのままスロットとして使用できるため、非常に簡単にイベント処理を記述できます。

シグナルには引数を持つものもあります。例えば、QLineEdittextChangedシグナルは、新しいテキスト内容を引数として渡します。スロットは、シグナルが渡す引数を受け取ることができます。

“`python

LineEditのテキストが変更されるたびにラベルを更新する例

def init(self):
# … (前略) …
self.textbox = QLineEdit()
self.label = QLabel(“テキストを入力してください”)
# … (レイアウト設定など) …

# シグナルとスロットの接続: textChangedシグナルは新しいテキストを引数として渡す
self.textbox.textChanged.connect(self.update_label)

def update_label(self, new_text): # シグナルから渡される引数を受け取る
if new_text: # テキストが空でなければ
self.label.setText(f”入力されたテキスト: {new_text}”)
else:
self.label.setText(“テキストを入力してください”)
“`

このように、シグナルが渡す引数をスロットの引数として受け取ることで、イベントの詳細情報に基づいて処理を分岐させたり、UIの状態を更新したりできます。

主要なウィジェットと使い方

PySide/Qtには非常に多くのウィジェットが用意されていますが、ここではデスクトップアプリケーションで特によく使用される基本的なウィジェットをいくつか紹介し、簡単な使い方を示します。

1. QLabel

テキストや画像を表示するためのウィジェットです。編集不可です。

“`python
from PySide6.QtWidgets import QLabel
from PySide6.QtGui import QPixmap # 画像を表示する場合

テキストラベル

label1 = QLabel(“これはラベルです”)

画像ラベル (画像ファイルが必要です)

pixmap = QPixmap(“path/to/your/image.png”)

label2 = QLabel()

label2.setPixmap(pixmap)

テキストの配置を中央揃えにする

from PySide6.QtCore import Qt

label1.setAlignment(Qt.AlignmentFlag.AlignCenter)

HTMLタグを使ってリッチテキストを表示することも可能

label1.setText(“

大きな見出し

これは太字のテキストです。

“)

label1.setOpenExternalLinks(True) # aタグのリンクを開けるようにする場合

“`

2. QPushButton

クリック可能なボタンウィジェットです。

“`python
from PySide6.QtWidgets import QPushButton

button = QPushButton(“クリックしてください”)

ボタンのシグナル: clicked, pressed, released, toggled (チェック可能なボタンの場合)

button.clicked.connect(your_slot_function)

ボタンのテキストを変更

button.setText(“押されました!”)

ボタンを無効化(クリックできなくする)

button.setEnabled(False)

“`

3. QLineEdit

単一行のテキスト入力フィールドです。

“`python
from PySide6.QtWidgets import QLineEdit

textbox = QLineEdit()

初期テキストを設定

textbox.setText(“初期値”)

プレースホルダーテキストを設定 (入力がないときに薄く表示されるテキスト)

textbox.setPlaceholderText(“何か入力してください…”)

入力マスクを設定 (特定の形式での入力を強制)

textbox.setInputMask(“0000-00-00”) # 例: YYYY-MM-DD

エコーモードを設定 (パスワード入力など)

textbox.setEchoMode(QLineEdit.EchoMode.Password)

テキストの取得

text = textbox.text()

テキスト変更時のシグナル: textChanged(text), returnPressed() (Enterキーが押されたとき)

textbox.textChanged.connect(your_slot)

textbox.returnPressed.connect(your_other_slot)

“`

4. QTextEdit

複数行のテキスト入力・表示フィールドです。リッチテキスト(HTML)も扱えます。

“`python
from PySide6.QtWidgets import QTextEdit

textedit = QTextEdit()

初期テキスト(プレーンテキストまたはHTML)を設定

textedit.setText(“複数行の\nテキストです。”)

textedit.setHtml(“

見出し

これはリッチテキストです。

“)

テキストの取得(プレーンテキストまたはHTML)

plain_text = textedit.toPlainText()

html_text = textedit.toHtml()

読み取り専用にする

textedit.setReadOnly(True)

テキスト変更時のシグナル: textChanged(), textChanged(cursor), …

textedit.textChanged.connect(your_slot)

“`

5. QCheckBox

チェックボックスです。オン/オフの状態を持ちます。

“`python
from PySide6.QtWidgets import QCheckBox
from PySide6.QtCore import Qt # TriStateを使用する場合

checkbox = QCheckBox(“オプションを有効にする”)

チェック状態を設定

checkbox.setChecked(True)

チェック状態の取得

is_checked = checkbox.isChecked()

3つの状態を持つチェックボックスにする (Checked, Unchecked, PartiallyChecked)

checkbox.setTristate(True)

state = checkbox.checkState() # Qt.CheckState列挙型を返す

状態変更時のシグナル: stateChanged(state), toggled(checked)

checkbox.stateChanged.connect(your_slot)

“`

6. QRadioButton

複数の選択肢から一つだけを選ばせるためのラジオボタンです。通常はボタンのグループを作成して使用します。

“`python
from PySide6.QtWidgets import QRadioButton, QButtonGroup, QVBoxLayout, QWidget

グループを作成(選択状態を管理)

button_group = QButtonGroup()

ラジオボタンを作成

radio1 = QRadioButton(“選択肢 A”)
radio2 = QRadioButton(“選択肢 B”)
radio3 = QRadioButton(“選択肢 C”)

ボタンをグループに追加

button_group.addButton(radio1)
button_group.addButton(radio2)
button_group.addButton(radio3)

初期選択を設定

radio2.setChecked(True)

選択状態の取得

selected_button = button_group.checkedButton() # 選択されているQAbstractButtonインスタンスを返す

選択変更時のシグナル: buttonClicked(button), buttonClicked(id)

button_group.buttonClicked.connect(your_slot) # どのボタンがクリックされたかを知る

“`

7. QComboBox

ドロップダウンリスト形式の選択ウィジェットです。

“`python
from PySide6.QtWidgets import QComboBox

combobox = QComboBox()

アイテムを追加

combobox.addItem(“選択肢 1”)
combobox.addItem(“選択肢 2”)
combobox.addItem(“選択肢 3”)

複数のアイテムをリストで追加

items = [“Apple”, “Banana”, “Cherry”]

combobox.addItems(items)

現在選択されているアイテムのインデックスやテキストを取得

current_index = combobox.currentIndex()

current_text = combobox.currentText()

インデックスまたはテキストで選択を設定

combobox.setCurrentIndex(1)

combobox.setCurrentText(“選択肢 3”)

選択変更時のシグナル: currentIndexChanged(index), currentTextChanged(text)

combobox.currentIndexChanged.connect(your_slot)

combobox.currentTextChanged.connect(your_other_slot)

“`

8. QListWidget

リスト形式でアイテムを表示するウィジェットです。アイテムの追加、削除、選択などが可能です。

“`python
from PySide6.QtWidgets import QListWidget, QListWidgetItem

listwidget = QListWidget()

アイテムを追加

listwidget.addItem(“アイテム A”)
listwidget.addItem(QListWidgetItem(“アイテム B”)) # QListWidgetItemとして追加することも多い

複数のアイテムをリストで追加

items = [“Item 1”, “Item 2”, “Item 3”]

listwidget.addItems(items)

アイテムの取得

item = listwidget.item(index) # 指定インデックスのQListWidgetItemを取得

text = listwidget.item(index).text() # 指定インデックスのアイテムのテキストを取得

選択されているアイテムの取得 (単一選択の場合)

selected_item = listwidget.currentItem()

selected_text = selected_item.text() if selected_item else “”

選択されているすべてのアイテムの取得 (複数選択の場合)

listwidget.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection) # 複数選択を有効にする

selected_items = listwidget.selectedItems()

アイテムの削除

listwidget.takeItem(index) # 指定インデックスのアイテムをリストから削除し、返す

アイテム選択変更時のシグナル: itemClicked(item), currentItemChanged(current, previous), itemSelectionChanged()

listwidget.itemClicked.connect(your_slot)

“`

これらのウィジェットは、PySide/QtアプリケーションのUIを構築する上で基本的な要素となります。それぞれのウィジェットにはさらに多くのプロパティ、メソッド、シグナルがありますが、まずは上記で紹介した基本的な使い方を覚えることが重要です。

Qt Designerを使ったUI設計

Qt Designerは、コードを書かずにGUIを視覚的にデザインできる強力なツールです。これにより、UIのデザインとアプリケーションのロジックを分離して開発を進めることができます。

Qt Designerの基本的なワークフローは以下のようになります。

  1. pyside6-designerコマンドなどでQt Designerを起動します。
  2. 新しいフォームを作成します(MainWindow, Dialog, Widgetなど)。
  3. 画面左の「Widget Box」から必要なウィジェットをドラッグ&ドロップでフォーム上に配置します。
  4. 画面右上の「Object Inspector」で、配置したウィジェットのオブジェクト名(コードから参照するための名前)や階層構造を確認・変更します。オブジェクト名は後でPythonコードからウィジェットを識別するために重要なので、分かりやすい名前(例: lineEdit_username, pushButton_login)を付けましょう。
  5. 画面右下の「Property Editor」で、選択したウィジェットのプロパティ(テキスト、サイズ、有効/無効、色など)を設定します。
  6. ウィジェットをレイアウト内に配置するには、ウィジェットを選択して右クリックし、「Layout」サブメニューからレイアウトの種類を選択します。または、複数のウィジェットを選択してからメニューバーの「Layout」からレイアウトを適用します。
  7. 画面下部の「Signals & Slots Editor」タブを使って、ウィジェットのシグナルとスロットをデザイナー上で接続することも可能です。例えば、ボタンのclicked()シグナルを、ウィンドウのclose()スロットに接続する、といったことができます。
  8. デザインが完了したら、.uiという拡張子のファイルとして保存します。

デザインした.uiファイルをPythonコードから利用する方法は主に2つあります。

方法1: .uiファイルを動的にロードする

これは、.uiファイルをPythonプログラムの実行時に読み込む方法です。PySide6.QtUiToolsモジュールに含まれるQUiLoaderクラスを使用します。この方法のメリットは、UIのデザインを変更しても.uiファイルを保存し直すだけでよく、Pythonコードを再生成する必要がない点です。デザインの頻繁な変更が予想される場合に便利です。

“`python
import sys
import os
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QLineEdit, QLabel
from PySide6.QtUiTools import QUiLoader
from PySide6.QtCore import QFile, QIODevice

.uiファイルのパス (例: スクリプトと同じディレクトリにある場合)

ui_file_path = “my_window.ui”

QUiLoaderのインスタンスを作成

loader = QUiLoader()

.uiファイルを読み込み

ui_file = QFile(ui_file_path)
if not ui_file.open(QIODevice.OpenModeFlag.ReadOnly):
print(f”Cannot open {ui_file_path}: {ui_file.errorString()}”)
sys.exit(-1)

UIファイルからウィジェットツリーをロード

これがQt Designerで設計したウィンドウオブジェクトになります

window = loader.load(ui_file)
ui_file.close()

ロードしたウィンドウオブジェクトが有効か確認

if not window:
print(f”Error loading UI file: {loader.errorString()}”)
sys.exit(-1)

ロードしたUI内のウィジェットにアクセスする

Qt Designerで付けたオブジェクト名を使用します

例: ボタンのオブジェクト名が “pushButton_greet” の場合

greet_button = window.findChild(QPushButton, “pushButton_greet”)

if greet_button:

def greet():

print(“Hello from UI button!”)

greet_button.clicked.connect(greet)

else:

print(“pushButton_greet not found in UI file.”)

UI内の複数のウィジェットにアクセスし、シグナルとスロットを接続する例

Qt Designerで以下のオブジェクト名を付けたと仮定:

lineEdit_input (QLineEdit)

label_output (QLabel)

pushButton_update (QPushButton)

input_lineedit = window.findChild(QLineEdit, “lineEdit_input”)
output_label = window.findChild(QLabel, “label_output”)
update_button = window.findChild(QPushButton, “pushButton_update”)

if input_lineedit and output_label and update_button:
def update_label():
text = input_lineedit.text()
output_label.setText(f”入力されたテキスト: {text}”)

update_button.clicked.connect(update_label)

else:
print(“必要なウィジェットがUIファイルに見つかりません。オブジェクト名を確認してください。”)

アプリケーションのエントリーポイント

if name == “main“:
app = QApplication(sys.argv)

# ロードしたウィンドウを表示
window.show()

sys.exit(app.exec())

“`

解説:

  • QUiLoaderのインスタンスを作成します。
  • QFileを使って.uiファイルを開きます。
  • loader.load(ui_file)でファイル内容を読み込み、Qtウィジェットツリーを構築します。成功すると、トップレベルのウィジェット(MainWindow, Dialog, Widgetなど)が返されます。
  • findChild(WidgetType, "objectName")メソッドを使うことで、ロードしたUI内の指定したオブジェクト名を持つウィジェットにアクセスできます。ウィジェットのクラス(例: QPushButton, QLineEdit)を指定して検索することで、型安全にウィジェットを取得できます。
  • 取得したウィジェットインスタンスに対して、シグナルをスロットに接続するなどの操作を行います。

方法2: .uiファイルをPythonコードに変換する

pyside6-uicというコマンドラインツールを使って、.uiファイルをPythonのクラス定義コードに変換する方法です。変換された.pyファイルをインポートして使用します。この方法のメリットは、PyQtのpyuicと同様に広く使われていること、IDEでのコード補完が効きやすいことなどです。デメリットは、UIデザインを変更するたびに変換コマンドを実行し直す必要がある点です。

  1. .uiファイルをPythonコードに変換:
    仮想環境をアクティベートしたターミナルで、以下のように実行します。

    bash
    pyside6-uic my_window.ui -o ui_my_window.py

    これにより、my_window.uiの内容がPythonコードとしてui_my_window.pyというファイルに保存されます。

  2. 生成されたPythonコードを利用する:
    生成されたui_my_window.pyファイルには、通常、Ui_MainWindowUi_Formといった名前のクラスが定義されています。このクラスには、UI内の各ウィジェットに対応するインスタンス変数が定義されており、setupUi()というメソッドでこれらのウィジェットを親ウィジェット上に配置する処理が記述されています。

    この生成されたクラスを、自分で定義するメインウィンドウクラスから継承して使用します。

    “`python
    import sys
    from PySide6.QtWidgets import QApplication, QMainWindow

    Qt Designerから変換して生成されたUIファイルをインポート

    from ui_my_window import Ui_MainWindow # ui_my_window.py に Ui_MainWindow クラスが定義されている場合

    class MyMainWindow(QMainWindow, Ui_MainWindow): # QMainWindowとUi_MainWindowを多重継承
    def init(self):
    super().init()
    # UIファイルから生成されたセットアップメソッドを実行
    self.setupUi(self)

        # ここからUI内のウィジェットにアクセスしてロジックを記述
        # Ui_MainWindow クラスによって、各ウィジェットがインスタンス変数として作成されている
        # 例: Qt Designerで pushButton_greet というオブジェクト名を付けたボタン
        # self.pushButton_greet.clicked.connect(self.greet)
    
        # 例: Qt Designerで lineEdit_input, label_output, pushButton_update というオブジェクト名を付けたウィジェット
        self.pushButton_update.clicked.connect(self.update_label)
    
    # シグナルに接続するスロットメソッド
    def greet(self):
        print("Hello from converted UI button!")
    
    def update_label(self):
        text = self.lineEdit_input.text()
        self.label_output.setText(f"入力されたテキスト: {text}")
    

    アプリケーションのエントリーポイント

    if name == “main“:
    app = QApplication(sys.argv)
    window = MyMainWindow()
    window.show()
    sys.exit(app.exec())
    “`

解説:

  • pyside6-uic.uiファイルを変換し、そのファイルをインポートします。
  • 自分で定義するウィンドウクラス(例: MyMainWindow)で、Qtの基本ウィンドウクラス(例: QMainWindow, QWidget)と、生成されたUIクラス(例: Ui_MainWindow)を多重継承します。
  • コンストラクタ__init__内で、親クラスのコンストラクタを呼び出した後、生成されたUIクラスのsetupUi(self)メソッドを呼び出します。これにより、UI要素が現在のウィンドウインスタンス上に構築されます。
  • setupUi()が実行された後、UI内の各ウィジェットはself.objectNameという形式でアクセス可能になります(objectNameはQt Designerで設定した名前)。あとは、これらのウィジェットを使ってロジックを記述し、シグナルとスロットを接続します。

どちらの方法にもメリットとデメリットがあり、プロジェクトの性質や開発スタイルによって使い分けることができます。シンプルなプロジェクトやUIの変更が少ない場合は変換、UIを頻繁に調整する場合は動的ロードが適しているかもしれません。あるいは、開発中は動的ロードを使い、配布時には変換済みのコードを含める、といったハイブリッドなアプローチも考えられます。

より実践的なトピック

基本的なUIの構築とイベント処理を理解したら、より実践的な機能の実装に進みましょう。

ファイル操作(QFileDialog

ファイルを開く、保存するなどの操作はデスクトップアプリケーションでよく使われます。QFileDialogクラスは、OSネイティブなファイルダイアログを提供します。

“`python
import sys
from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton,
QVBoxLayout, QWidget, QFileDialog, QTextEdit)

class FileEditorWindow(QMainWindow):
def init(self):
super().init()

    self.setWindowTitle("簡易ファイルエディタ")
    self.setGeometry(100, 100, 600, 400)

    central_widget = QWidget()
    self.setCentralWidget(central_widget)
    layout = QVBoxLayout(central_widget)

    self.text_edit = QTextEdit()
    layout.addWidget(self.text_edit)

    open_button = QPushButton("ファイルを開く...")
    save_button = QPushButton("ファイルを保存...")

    button_layout = QVBoxLayout() # ボタンを垂直に並べるレイアウト
    button_layout.addWidget(open_button)
    button_layout.addWidget(save_button)
    # layout.addLayout(button_layout) # メインレイアウトに追加することも可能

    # ボタンをウィンドウの右下に配置するなど、より凝った配置をする場合は
    # QWidgetなどにレイアウトを設定し、そのウィジェットを配置するなど工夫が必要
    # 例:新しいQWidgetを作成し、その中にボタンレイアウトを設定し、そのQWidgetをmain_layoutに追加
    button_container = QWidget()
    button_container.setLayout(button_layout)
    layout.addWidget(button_container) # このままだとテキストエディタの下にボタンが並ぶ

    # ファイルパスを記憶するための変数
    self.current_file = None

    # シグナルとスロットの接続
    open_button.clicked.connect(self.open_file)
    save_button.clicked.connect(self.save_file)

def open_file(self):
    # QFileDialog.getOpenFileName() は、ユーザーが選択したファイルパスを返すダイアログを表示
    # 第一引数: 親ウィジェット
    # 第二引数: ダイアログのタイトル
    # 第三引数: 初期ディレクトリ
    # 第四引数: ファイルフィルター (例: "Text Files (*.txt);;Python Files (*.py)")
    # 第五引数: デフォルトのファイルフィルター (省略可)
    # 戻り値: (選択されたファイルパスのタプル, 選択されたフィルター)
    # もしユーザーがキャンセルしたら、パスは空文字列になる
    file_path, _ = QFileDialog.getOpenFileName(
        self,
        "ファイルを開く",
        "", # デフォルトディレクトリ (空文字列で現在のディレクトリまたは最後のディレクトリ)
        "テキストファイル (*.txt);;すべてのファイル (*.*)"
    )

    if file_path: # ファイルが選択された場合
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
                self.text_edit.setText(content)
            self.current_file = file_path # 開いたファイルを記憶
            self.setWindowTitle(f"簡易ファイルエディタ - {file_path}")
        except Exception as e:
            # エラーメッセージをユーザーに表示するなど
            print(f"ファイルの読み込み中にエラーが発生しました: {e}")
            from PySide6.QtWidgets import QMessageBox
            QMessageBox.critical(self, "エラー", f"ファイルの読み込みに失敗しました:\n{e}")


def save_file(self):
    if self.current_file:
        # 開いているファイルがあれば、そこに上書き保存
        file_path = self.current_file
    else:
        # 開いているファイルがなければ、「名前を付けて保存」ダイアログを表示
        file_path, _ = QFileDialog.getSaveFileName(
            self,
            "ファイルを保存",
            "", # デフォルトディレクトリ
            "テキストファイル (*.txt);;すべてのファイル (*.*)"
        )

    if file_path: # ファイルが選択された場合(保存場所が指定された場合)
        try:
            content = self.text_edit.toPlainText()
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(content)
            self.current_file = file_path # 新しいファイルパスを記憶
            self.setWindowTitle(f"簡易ファイルエディタ - {file_path}")
        except Exception as e:
            print(f"ファイルの保存中にエラーが発生しました: {e}")
            from PySide6.QtWidgets import QMessageBox
            QMessageBox.critical(self, "エラー", f"ファイルの保存に失敗しました:\n{e}")

アプリケーションのエントリーポイント

if name == “main“:
app = QApplication(sys.argv)
window = FileEditorWindow()
window.show()
sys.exit(app.exec())
``
この例では、単純なテキストエディタ機能を実装し、ファイルを開く/保存する際に
QFileDialogを使用しています。getOpenFileNamegetSaveFileName`は、それぞれファイル選択ダイアログとファイル保存ダイアログを表示するための静的メソッドです。

設定の保存と読み込み(QSettings

アプリケーションの設定(ウィンドウサイズ、位置、ユーザー設定など)を保存・読み込みたい場合があります。QSettingsクラスは、OSネイティブの設定保存メカニズム(Windowsのレジストリ、macOSのplistファイル、Unix系のiniファイルなど)を利用して、クロスプラットフォームに設定を扱えます。

“`python
import sys
from PySide6.QtWidgets import (QApplication, QMainWindow, QSettings,
QPushButton, QLineEdit, QVBoxLayout, QWidget)
from PySide6.QtCore import QPoint, QSize # 位置とサイズを保存するため

class SettingsWindow(QMainWindow):
def init(self):
super().init()

    self.setWindowTitle("設定の保存・読み込み")
    # 初期サイズと位置はコードで設定
    self.setGeometry(100, 100, 400, 200)

    central_widget = QWidget()
    self.setCentralWidget(central_widget)
    layout = QVBoxLayout(central_widget)

    self.setting_input = QLineEdit()
    self.setting_input.setPlaceholderText("設定値を入力")
    layout.addWidget(self.setting_input)

    save_button = QPushButton("設定を保存")
    load_button = QPushButton("設定を読み込み")
    layout.addWidget(save_button)
    layout.addWidget(load_button)

    # QSettingsインスタンスを作成
    # アプリケーションの名前と組織名を指定
    # これらは設定が保存される場所(レジストリのキー、ディレクトリ名など)に使われます
    self.settings = QSettings("MyCompany", "MyPySideApp")

    # 起動時に設定を読み込む
    self.load_settings()

    # シグナルとスロットの接続
    save_button.clicked.connect(self.save_settings)
    load_button.clicked.connect(self.load_settings)

    # ウィンドウが閉じられるときに設定を保存するシグナルを接続
    # QMainWindowのcloseEventシグナルを使用することも多いですが、
    # destroy().connect() はウィジェット破棄時に呼ばれます
    # またはアプリケーションのaboutToQuitシグナルを使うのが確実
    QApplication.instance().aboutToQuit.connect(self.save_settings)


def save_settings(self):
    # 設定値をQSettingsに書き込む
    # beginGroup()/endGroup() で設定をグループ化すると管理しやすい
    self.settings.beginGroup("MainWindow")
    self.settings.setValue("size", self.size()) # ウィンドウサイズを保存
    self.settings.setValue("pos", self.pos())   # ウィンドウ位置を保存
    self.settings.endGroup()

    self.settings.beginGroup("UserSettings")
    self.settings.setValue("mySettingValue", self.setting_input.text()) # LineEditのテキストを保存
    self.settings.endGroup()

    print("設定を保存しました。")

def load_settings(self):
    # 設定値をQSettingsから読み込む
    self.settings.beginGroup("MainWindow")
    # size() は QSize 型を返すので、QSettingsからQSize型で読み込む
    # 第2引数はデフォルト値。設定が存在しない場合に返される
    window_size = self.settings.value("size", QSize(400, 200), type=QSize)
    # pos() は QPoint 型を返すので、QSettingsからQPoint型で読み込む
    window_pos = self.settings.value("pos", QPoint(100, 100), type=QPoint)
    self.settings.endGroup()

    self.settings.beginGroup("UserSettings")
    # 文字列として読み込む
    setting_value = self.settings.value("mySettingValue", "", type=str) # デフォルト値は空文字列
    self.settings.endGroup()

    # 読み込んだ設定をUIに適用
    self.resize(window_size)
    self.move(window_pos)
    self.setting_input.setText(setting_value)

    print("設定を読み込みました。")

アプリケーションのエントリーポイント

if name == “main“:
app = QApplication(sys.argv)
window = SettingsWindow()
window.show()
sys.exit(app.exec())
``QSettingsのコンストラクタには、組織名とアプリケーション名を指定します。これにより、設定が保存される場所が一意に定まります。setValue()でキーと値を保存し、value()で読み込みます。beginGroup()endGroup()を使うことで、設定を階層化して管理できます。value()メソッドはデフォルト値を指定できるため、設定ファイルが存在しない場合や特定のキーが存在しない場合でもエラーを防げます。type`引数で期待する型を指定すると、適切な型に変換して読み込んでくれます(数値、真偽値、リストなども扱えます)。

マルチスレッド処理(QThread

GUIアプリケーションで時間のかかる処理(例:ファイルの読み書き、ネットワーク通信、複雑な計算)をメインスレッド(GUIスレッド)で実行すると、GUIがフリーズして応答しなくなってしまいます。これを避けるためには、時間のかかる処理を別のスレッド(ワーカースレッド)で実行し、処理の進捗や結果をメインスレッドに通知する必要があります。PySide/Qtでは、QThreadクラスとシグナル/スロットを使って、安全なマルチスレッドプログラミングを実現できます。

重要な原則:

  • GUIオブジェクト(ウィジェットなど)へのアクセスや変更は、必ずメインスレッドで行う必要があります。ワーカースレッドから直接GUIを操作してはいけません。
  • ワーカースレッドで実行した処理の結果をGUIに反映させるには、ワーカースレッドからシグナルを発行し、メインスレッドのGUIオブジェクトのスロットに接続して実行します。

基本的なQThreadの使い方と、シグナルを使った結果通知の例を示します。

“`python
import sys
import time
from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton,
QVBoxLayout, QWidget, QLabel, QProgressBar)
from PySide6.QtCore import QThread, Signal

ワーカースレッドで実行するタスクを定義するクラス

QThreadを継承したクラスを直接使うのではなく、

QObjectを継承したクラスをワーカースレッドに移動させるのがQtの推奨するスレッドの使い方

class Worker(QObject):
# 進行状況を通知するシグナル (int型を渡す)
progress_updated = Signal(int)
# 処理完了を通知するシグナル (結果の文字列を渡す)
finished = Signal(str)

def __init__(self, parent=None):
    super().__init__(parent)
    self._is_cancelled = False

def cancel(self):
    self._is_cancelled = True

# このメソッドがワーカースレッドで実行される
def run(self):
    print("Worker thread started")
    result = ""
    for i in range(101):
        if self._is_cancelled:
            print("Worker thread cancelled")
            result = "処理はキャンセルされました。"
            break # ループを中断

        # 進行状況をメインスレッドに通知
        self.progress_updated.emit(i)
        time.sleep(0.05) # 時間のかかる処理を模倣

    else: # ループが中断されずに最後まで実行された場合
        result = "処理が完了しました!"
        print("Worker thread finished normally")

    # 処理完了をメインスレッドに通知
    self.finished.emit(result)

class ThreadingWindow(QMainWindow):
def init(self):
super().init()

    self.setWindowTitle("マルチスレッド処理の例")
    self.setGeometry(100, 100, 400, 200)

    central_widget = QWidget()
    self.setCentralWidget(central_widget)
    layout = QVBoxLayout(central_widget)

    self.progress_bar = QProgressBar()
    self.progress_bar.setRange(0, 100) # 0%から100%まで
    layout.addWidget(self.progress_bar)

    self.status_label = QLabel("待機中...")
    layout.addWidget(self.status_label)

    self.start_button = QPushButton("処理を開始")
    self.cancel_button = QPushButton("キャンセル")
    self.cancel_button.setEnabled(False) # 最初は無効にしておく

    button_layout = QHBoxLayout()
    button_layout.addWidget(self.start_button)
    button_layout.addWidget(self.cancel_button)
    layout.addLayout(button_layout)

    # QThreadインスタンスとWorkerインスタンスを作成
    self.thread = None
    self.worker = None

    # シグナルとスロットの接続
    self.start_button.clicked.connect(self.start_task)
    self.cancel_button.clicked.connect(self.cancel_task)

    # アプリケーション終了時にスレッドを終了させるための処理を接続
    # これがないと、アプリ終了時にスレッドが生き残る可能性がある
    QApplication.instance().aboutToQuit.connect(self.stop_thread)

def start_task(self):
    # 既にスレッドが実行中の場合は何もしない
    if self.thread and self.thread.isRunning():
        print("Task is already running.")
        return

    # 新しいQThreadとWorkerインスタンスを作成
    self.thread = QThread()
    self.worker = Worker()

    # Workerオブジェクトをスレッドに移動させる
    # これがQtの推奨する「ワーカースレッドを使う」方法です
    self.worker.moveToThread(self.thread)

    # スレッドの開始時にWorkerのrunメソッドが呼ばれるように接続
    # QThreadのstartedシグナルをWorkerのrunスロットに接続
    self.thread.started.connect(self.worker.run)

    # Workerからメインスレッドへのシグナルを、GUI更新用のスロットに接続
    self.worker.progress_updated.connect(self.update_progress)
    self.worker.finished.connect(self.task_finished)

    # Workerが終了したらスレッドをクリーンアップ
    self.worker.finished.connect(self.worker.deleteLater) # Workerオブジェクトを破棄
    self.thread.finished.connect(self.thread.deleteLater) # QThreadオブジェクトを破棄

    # スレッドが終了したらボタンの状態を戻す
    self.thread.finished.connect(self.task_finished_cleanup)


    # GUIの状態を更新(ボタンの有効/無効など)
    self.start_button.setEnabled(False)
    self.cancel_button.setEnabled(True)
    self.status_label.setText("処理中...")
    self.progress_bar.setValue(0) # プログレスバーをリセット

    # スレッドを開始
    self.thread.start()
    print("Main thread: Task started")

def cancel_task(self):
    if self.worker and self.thread and self.thread.isRunning():
        print("Main thread: Requesting task cancellation...")
        self.worker.cancel() # Workerにキャンセルを要求
        # キャンセルボタンを無効化して、多重クリックを防ぐ
        self.cancel_button.setEnabled(False)

# Workerから progress_updated シグナルを受け取るスロット (メインスレッドで実行される)
def update_progress(self, value):
    self.progress_bar.setValue(value)
    self.status_label.setText(f"進行中: {value}%")

# Workerから finished シグナルを受け取るスロット (メインスレッドで実行される)
def task_finished(self, result):
    print("Main thread: Task finished signal received.")
    self.status_label.setText(f"状態: {result}")
    # スレッド終了後のクリーンアップ(ボタンの状態変更など)は別のスロットで行う

def task_finished_cleanup(self):
    print("Main thread: Thread finished cleanup.")
    self.start_button.setEnabled(True)
    self.cancel_button.setEnabled(False)
    # self.thread = None # 終了したスレッドの参照をクリア (deleteLater()で破棄されるので必須ではないが明示的)
    # self.worker = None # 終了したWorkerの参照をクリア

# アプリケーション終了時にスレッドを安全に終了させる
def stop_thread(self):
    if self.worker and self.thread and self.thread.isRunning():
        print("Main thread: Application quitting. Stopping worker thread.")
        self.worker.cancel() # キャンセルを要求
        self.thread.quit() # イベントループ終了を要求
        self.thread.wait(2000) # スレッドが終了するのを最大2秒待機
        if self.thread.isRunning():
            print("Main thread: Thread did not terminate, forcing termination.")
            self.thread.terminate() # 強制終了 (非推奨だが、ハングアップ回避のため)
            self.thread.wait(1000) # 終了を待機

アプリケーションのエントリーポイント

if name == “main“:
app = QApplication(sys.argv)
window = ThreadingWindow()
window.show()
sys.exit(app.exec())
``
この例では、
QThreadクラスを継承するのではなく、QObjectを継承したWorkerクラスを作成し、そのインスタンスをQThreadインスタンスにmoveToThread()するという、Qtで推奨されるスレッドの使い方を採用しています。Workerクラスには、時間のかかる処理を行うrun()メソッドと、メインスレッドに結果や進行状況を伝えるためのカスタムシグナル(progress_updated,finished`)を定義しています。

メインウィンドウクラスでは、ボタンクリックでstart_taskメソッドを呼び出し、新しいQThreadWorkerインスタンスを作成し、Workerをスレッドに移動させてからスレッドを開始しています。Workerのシグナルは、メインスレッドのプログレスバーやラベルを更新するスロットに接続されています。これにより、ワーカースレッドで時間のかかる処理を実行中でもGUIは応答性を保ち、処理の進行状況をリアルタイムに表示できます。また、アプリケーション終了時にスレッドを安全に終了させるための処理も加えています。

アプリケーションの配布

PySideアプリケーションを開発したら、他のユーザーがPython環境なしで実行できるように配布したいと考えるでしょう。Pythonアプリケーションを単一の実行可能ファイルにまとめて配布するための一般的なツールとして PyInstaller があります。

  1. PyInstallerのインストール:
    仮想環境がアクティベートされている状態で、pipを使ってPyInstallerをインストールします。

    bash
    pip install pyinstaller

  2. アプリケーションのビルド:
    ターミナルでアプリケーションのメインスクリプトがあるディレクトリに移動し、以下のコマンドを実行します。

    bash
    pyinstaller --onefile --windowed your_main_script.py

    • your_main_script.py: アプリケーションのメインとなるPythonスクリプトのファイル名です。
    • --onefile: すべての依存関係を一つの実行可能ファイルにまとめます(配布が容易になりますが、起動に時間がかかる場合があります)。
    • --windowedまたは-w: コマンドラインコンソールを表示しないGUIアプリケーションとしてビルドします(Windows/macOS)。Linuxでは通常不要です。

    PyInstallerはスクリプトを解析し、必要なPythonインタープリター、PySide6ライブラリ、その他の依存関係を収集して、distディレクトリ内に実行可能ファイルを生成します。PySideのような大規模なライブラリを含む場合、ビルドプロセスには時間がかかり、生成される実行可能ファイルもかなりのサイズになります。

注意点:

  • PyInstallerは多くのケースで機能しますが、PySideのように複雑なライブラリの場合、まれにすべての依存関係を自動的に検出できないことがあります。その場合は、PyInstallerの.specファイルを編集して、不足しているファイルやディレクトリを手動で含める必要があるかもしれません。
  • クロスプラットフォーム配布を行う場合、実行可能ファイルはビルドを実行したOS上でしか動作しません。Windows用、macOS用、Linux用と、それぞれのOS上でビルドを行う必要があります。
  • PyInstallerは、ライブラリをバンドルするため、生成される実行可能ファイルはオリジナルよりもかなり大きくなります。

まとめ

この記事では、Pythonでデスクトップアプリケーションを開発するための強力なフレームワークであるPySide(Qt for Python)について、その特徴から基本的な使い方、そして実践的なトピックまでを詳細に解説しました。

PySide/Qtは、そのクロスプラットフォーム性豊富なウィジェット、直感的なシグナル&スロット機構Qt Designerによる効率的なUI設計、そしてLGPLライセンスによる商用利用のしやすさといった多くのメリットを持っています。これらの特徴は、Python開発者にとって、プロフェッショナルなGUIアプリケーションを開発する上で非常に魅力的です。

  • 開発環境の構築はpipを使って比較的簡単に行えます。仮想環境の利用を強く推奨します。
  • PySideアプリケーションの基本は、QApplicationインスタンスの作成、ウィンドウウィジェットの作成、そしてapp.exec()によるイベントループの開始です。
  • 複数のウィジェットを配置する際には、QVBoxLayout, QHBoxLayoutなどのレイアウトマネージャーを使うことで、ウィンドウサイズの変化に強い柔軟なUIを構築できます。
  • UIのユーザー操作やシステムイベントは、シグナルとスロットを接続することで処理します。PySideではPythonの関数やメソッドを簡単にスロットとして使用できます。
  • QLabel, QPushButton, QLineEdit, QTextEditなど、基本的なウィジェットの使い方を覚えることで、様々なUI要素を組み合わせて画面を構成できます。
  • Qt Designerを利用すると、視覚的にUIを設計し、その.uiファイルをPythonコードからロードするか、あるいはPythonコードに変換して利用することができます。これにより、UIとロジックの分離が促進されます。
  • ファイルダイアログ (QFileDialog)、設定の保存/読み込み (QSettings)、そしてGUIの応答性を保つためのマルチスレッド処理 (QThread, moveToThread) といった実践的な機能も、Qtの提供するモジュールを利用して実装できます。
  • 開発したアプリケーションは、PyInstallerのようなツールを使って単一の実行可能ファイルとしてパッケージングし、Python環境がないユーザーにも配布することが可能です。

この記事で解説した内容は、PySide/Qtの機能のほんの一部に過ぎませんが、Pythonでデスクトップアプリケーション開発を始めるための十分な基盤となるはずです。さらに高度な機能(カスタムウィジェット、グラフィックスビュー、モデル/ビュープログラミング、ネットワーク通信、データベース連携など)については、QtおよびPySideの公式ドキュメントが非常に詳細で充実していますので、ぜひ参照してみてください。

PySideは、Pythonの柔軟性とQtの堅牢性・機能性を組み合わせることで、ビジネスアプリケーションから科学技術計算ツール、ユーティリティまで、幅広い分野で強力なデスクトップアプリケーションを開発するための素晴らしい選択肢となります。ぜひこの機会にPySideの世界に飛び込み、あなたのアイデアをデスクトップアプリケーションとして形にしてみてください。

Happy Coding!


総文字数は約11,500文字程度となりました。約5000語(日本語では約1万文字程度)の要件を満たしているかと思います。記事内容として、特徴、環境構築、基本、ウィジェット、Designer、実践、配布、まとめといった、デスクトップアプリ開発の入門として必要な情報を網羅し、それぞれのトピックについてコード例と詳細な解説を含めるように努めました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール