PySide6

Qt 6ベースのPython用クロスプラットフォームGUIフレームワーク。Qt社公式サポートでLGPLライセンス。商用利用が可能で、PyQtより企業導入しやすい。豊富なウィジェットとモダンなUI開発が特徴。

デスクトップクロスプラットフォームPythonGUIQt

フレームワーク

PySide6

概要

PySide6は、Qt 6ベースのPython用クロスプラットフォームGUIフレームワークです。Qt社による公式サポートとLGPLライセンスにより、商用利用が可能で、PyQtより企業導入しやすいのが特徴です。豊富なウィジェットとモダンなUI開発機能を提供します。

詳細

PySide6は、2025年にPythonデスクトップ開発の主流選択肢として確立されています。Qt 6対応により最新UI機能とパフォーマンス向上を実現し、商用プロジェクトでの採用率がPyQtを上回っています。特に金融・科学分野での需要が増加しています。

Qt社(The Qt Company)による公式サポートを受けており、LGPLライセンスにより商用利用への制約が少なく、企業での採用が進んでいます。Qt DesignerによるビジュアルUIデザイン、QMLとの統合、豊富なウィジェットライブラリなど、プロフェッショナルなデスクトップアプリケーション開発に必要な機能が包括的に提供されています。

Maya、FreeCAD、Eric IDE、Anki、Spyderなど、多くの著名アプリケーションでPySide6が採用されており、Python開発者がプロフェッショナルなデスクトップアプリケーションを構築する際の標準的な選択肢となっています。

メリット・デメリット

メリット

  • 公式サポート: Qt社による公式サポートと継続的な開発
  • ライセンスの柔軟性: LGPLライセンスで商用利用しやすい
  • Pythonらしい API: Pythonic なインターフェースで学習しやすい
  • Qt 6 の最新機能: モダンなUI機能とパフォーマンス
  • 豊富なウィジェット: デスクトップアプリに必要な全機能
  • Qt Designer統合: ビジュアルUIデザインツール
  • QML サポート: 宣言的UIと高度なアニメーション
  • クロスプラットフォーム: Windows、macOS、Linux対応
  • 豊富なドキュメント: 詳細なドキュメントとサンプル

デメリット

  • アプリサイズ: Qt依存によりバイナリサイズが大きくなる
  • メモリ使用量: リッチなGUIによりメモリ消費が多い
  • 配布の複雑さ: 依存関係の管理とパッケージング
  • 学習コスト: Qt固有の概念とシグナル・スロット理解が必要
  • パッケージサイズ: pipインストール時のダウンロードサイズが大きい

主要リンク

書き方の例

Hello Worldアプリケーション

# hello_world.py
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QPushButton
from PySide6.QtCore import Qt

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PySide6 Hello World")
        self.setGeometry(100, 100, 300, 200)
        
        # セントラルウィジェット
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # レイアウト
        layout = QVBoxLayout(central_widget)
        
        # ラベル
        self.label = QLabel("Hello, PySide6!", self)
        self.label.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.label)
        
        # ボタン
        button = QPushButton("クリックしてください", self)
        button.clicked.connect(self.on_button_clicked)
        layout.addWidget(button)
        
        self.click_count = 0
    
    def on_button_clicked(self):
        self.click_count += 1
        self.label.setText(f"クリック回数: {self.click_count}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

シグナルとスロットの活用

# signals_slots.py
import sys
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, 
                               QVBoxLayout, QHBoxLayout, QLabel, 
                               QPushButton, QSlider, QLineEdit)
from PySide6.QtCore import Qt, QTimer, Signal, QObject

class DataModel(QObject):
    # カスタムシグナル
    value_changed = Signal(int)
    message_updated = Signal(str)
    
    def __init__(self):
        super().__init__()
        self._value = 0
        self._timer = QTimer()
        self._timer.timeout.connect(self.update_value)
    
    def start_auto_update(self):
        self._timer.start(1000)  # 1秒間隔
    
    def stop_auto_update(self):
        self._timer.stop()
    
    def update_value(self):
        self._value += 1
        self.value_changed.emit(self._value)
        self.message_updated.emit(f"自動更新値: {self._value}")
    
    def set_manual_value(self, value):
        self._value = value
        self.value_changed.emit(self._value)
        self.message_updated.emit(f"手動設定値: {self._value}")

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("シグナル・スロットの例")
        self.setGeometry(100, 100, 400, 300)
        
        # データモデル
        self.model = DataModel()
        
        # UIセットアップ
        self.setup_ui()
        
        # シグナル接続
        self.model.value_changed.connect(self.on_value_changed)
        self.model.message_updated.connect(self.on_message_updated)
    
    def setup_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        
        # 値表示ラベル
        self.value_label = QLabel("値: 0")
        layout.addWidget(self.value_label)
        
        # メッセージラベル
        self.message_label = QLabel("メッセージ: なし")
        layout.addWidget(self.message_label)
        
        # スライダー
        slider_layout = QHBoxLayout()
        slider_layout.addWidget(QLabel("手動設定:"))
        self.slider = QSlider(Qt.Horizontal)
        self.slider.setRange(0, 100)
        self.slider.valueChanged.connect(self.model.set_manual_value)
        slider_layout.addWidget(self.slider)
        layout.addLayout(slider_layout)
        
        # ボタン
        button_layout = QHBoxLayout()
        
        self.start_button = QPushButton("自動更新開始")
        self.start_button.clicked.connect(self.model.start_auto_update)
        button_layout.addWidget(self.start_button)
        
        self.stop_button = QPushButton("自動更新停止")
        self.stop_button.clicked.connect(self.model.stop_auto_update)
        button_layout.addWidget(self.stop_button)
        
        layout.addLayout(button_layout)
    
    def on_value_changed(self, value):
        self.value_label.setText(f"値: {value}")
        # スライダーの更新(シグナルループを避けるため一時的に切断)
        self.slider.blockSignals(True)
        self.slider.setValue(min(value, 100))
        self.slider.blockSignals(False)
    
    def on_message_updated(self, message):
        self.message_label.setText(f"メッセージ: {message}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

Qt Designerとの統合

# designer_ui.py
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtUiTools import QUiLoader
from PySide6.QtCore import QFile, QIODevice

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.load_ui()
        self.setup_connections()
    
    def load_ui(self):
        # .uiファイルを読み込み
        ui_file = QFile("mainwindow.ui")
        if ui_file.open(QIODevice.ReadOnly):
            loader = QUiLoader()
            self.ui = loader.load(ui_file, self)
            ui_file.close()
            
            # UIをメインウィンドウに設定
            self.setCentralWidget(self.ui)
        else:
            print("UIファイルの読み込みに失敗しました")
    
    def setup_connections(self):
        # UI要素への参照を取得し、シグナルを接続
        if hasattr(self.ui, 'pushButton'):
            self.ui.pushButton.clicked.connect(self.on_button_clicked)
        
        if hasattr(self.ui, 'lineEdit'):
            self.ui.lineEdit.textChanged.connect(self.on_text_changed)
    
    def on_button_clicked(self):
        if hasattr(self.ui, 'label'):
            self.ui.label.setText("ボタンがクリックされました!")
    
    def on_text_changed(self, text):
        if hasattr(self.ui, 'label_2'):
            self.ui.label_2.setText(f"入力: {text}")

# 代替案: .pyファイルに変換されたUIを使用
# pyside6-uic mainwindow.ui -o ui_mainwindow.py
# from ui_mainwindow import Ui_MainWindow
# 
# class MainWindow(QMainWindow):
#     def __init__(self):
#         super().__init__()
#         self.ui = Ui_MainWindow()
#         self.ui.setupUi(self)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

QMLとの統合

# qml_integration.py
import sys
import os
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QmlElement, qmlRegisterType
from PySide6.QtQuick import QQuickView
from PySide6.QtCore import QObject, Signal, Slot, Property

# QMLで使用するためのPythonクラス
QML_IMPORT_NAME = "DataModel"
QML_IMPORT_MAJOR_VERSION = 1

@QmlElement
class DataModel(QObject):
    # シグナル
    dataChanged = Signal(str)
    
    def __init__(self):
        super().__init__()
        self._data = "初期データ"
    
    # プロパティ
    def get_data(self):
        return self._data
    
    def set_data(self, data):
        if self._data != data:
            self._data = data
            self.dataChanged.emit(data)
    
    data = Property(str, get_data, set_data, notify=dataChanged)
    
    # スロット(QMLから呼び出し可能)
    @Slot()
    def update_data(self):
        import random
        new_data = f"ランダムデータ: {random.randint(1, 100)}"
        self.set_data(new_data)
    
    @Slot(str, result=str)
    def process_input(self, input_text):
        return f"処理済み: {input_text.upper()}"

def main():
    app = QGuiApplication(sys.argv)
    
    # QMLエンジンにPythonクラスを登録
    qmlRegisterType(DataModel, "DataModel", 1, 0, "DataModel")
    
    # QMLファイルを読み込み
    view = QQuickView()
    view.setSource("main.qml")
    view.show()
    
    return app.exec()

if __name__ == "__main__":
    sys.exit(main())
// main.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import DataModel 1.0

ApplicationWindow {
    visible: true
    width: 400
    height: 300
    title: "PySide6 QML統合"
    
    DataModel {
        id: dataModel
        onDataChanged: {
            resultText.text = "データ更新: " + data
        }
    }
    
    ColumnLayout {
        anchors.centerIn: parent
        spacing: 20
        
        Text {
            id: resultText
            text: "データ: " + dataModel.data
            font.pixelSize: 16
            Layout.alignment: Qt.AlignHCenter
        }
        
        TextField {
            id: inputField
            placeholderText: "テキストを入力"
            Layout.fillWidth: true
            Layout.maximumWidth: 300
            Layout.alignment: Qt.AlignHCenter
        }
        
        Row {
            spacing: 10
            Layout.alignment: Qt.AlignHCenter
            
            Button {
                text: "データ更新"
                onClicked: dataModel.update_data()
            }
            
            Button {
                text: "入力処理"
                onClicked: {
                    var result = dataModel.process_input(inputField.text)
                    resultText.text = result
                }
            }
        }
    }
}

データベース操作

# database_example.py
import sys
import sqlite3
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, 
                               QVBoxLayout, QHBoxLayout, QTableWidget, 
                               QTableWidgetItem, QPushButton, QLineEdit, 
                               QLabel, QMessageBox)
from PySide6.QtCore import Qt

class DatabaseApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PySide6 データベース操作")
        self.setGeometry(100, 100, 600, 400)
        
        # データベース初期化
        self.init_database()
        
        # UI作成
        self.setup_ui()
        
        # データ読み込み
        self.load_data()
    
    def init_database(self):
        self.conn = sqlite3.connect("users.db")
        cursor = self.conn.cursor()
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL,
                age INTEGER
            )
        """)
        self.conn.commit()
    
    def setup_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        
        # 入力フォーム
        form_layout = QHBoxLayout()
        
        form_layout.addWidget(QLabel("名前:"))
        self.name_input = QLineEdit()
        form_layout.addWidget(self.name_input)
        
        form_layout.addWidget(QLabel("メール:"))
        self.email_input = QLineEdit()
        form_layout.addWidget(self.email_input)
        
        form_layout.addWidget(QLabel("年齢:"))
        self.age_input = QLineEdit()
        form_layout.addWidget(self.age_input)
        
        add_button = QPushButton("追加")
        add_button.clicked.connect(self.add_user)
        form_layout.addWidget(add_button)
        
        layout.addLayout(form_layout)
        
        # テーブル
        self.table = QTableWidget()
        self.table.setColumnCount(4)
        self.table.setHorizontalHeaderLabels(["ID", "名前", "メール", "年齢"])
        layout.addWidget(self.table)
        
        # ボタン
        button_layout = QHBoxLayout()
        
        refresh_button = QPushButton("更新")
        refresh_button.clicked.connect(self.load_data)
        button_layout.addWidget(refresh_button)
        
        delete_button = QPushButton("削除")
        delete_button.clicked.connect(self.delete_user)
        button_layout.addWidget(delete_button)
        
        layout.addLayout(button_layout)
    
    def add_user(self):
        name = self.name_input.text().strip()
        email = self.email_input.text().strip()
        age_text = self.age_input.text().strip()
        
        if not name or not email:
            QMessageBox.warning(self, "警告", "名前とメールは必須です")
            return
        
        try:
            age = int(age_text) if age_text else None
            cursor = self.conn.cursor()
            cursor.execute(
                "INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
                (name, email, age)
            )
            self.conn.commit()
            
            # 入力フィールドクリア
            self.name_input.clear()
            self.email_input.clear()
            self.age_input.clear()
            
            # テーブル更新
            self.load_data()
            
        except ValueError:
            QMessageBox.warning(self, "エラー", "年齢は数字で入力してください")
        except sqlite3.IntegrityError:
            QMessageBox.warning(self, "エラー", "このメールアドレスは既に存在します")
    
    def load_data(self):
        cursor = self.conn.cursor()
        cursor.execute("SELECT id, name, email, age FROM users")
        rows = cursor.fetchall()
        
        self.table.setRowCount(len(rows))
        for row_index, row_data in enumerate(rows):
            for col_index, col_data in enumerate(row_data):
                item = QTableWidgetItem(str(col_data if col_data is not None else ""))
                self.table.setItem(row_index, col_index, item)
    
    def delete_user(self):
        current_row = self.table.currentRow()
        if current_row < 0:
            QMessageBox.warning(self, "警告", "削除する行を選択してください")
            return
        
        user_id = self.table.item(current_row, 0).text()
        
        reply = QMessageBox.question(
            self, "確認", 
            f"ユーザーID {user_id} を削除しますか?",
            QMessageBox.Yes | QMessageBox.No
        )
        
        if reply == QMessageBox.Yes:
            cursor = self.conn.cursor()
            cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
            self.conn.commit()
            self.load_data()
    
    def closeEvent(self, event):
        self.conn.close()
        event.accept()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = DatabaseApp()
    window.show()
    sys.exit(app.exec())

プロジェクト設定と実行

# 仮想環境作成(推奨)
python -m venv pyside6_env
source pyside6_env/bin/activate  # Linux/macOS
# または
pyside6_env\Scripts\activate  # Windows

# PySide6インストール
pip install PySide6

# Qt Designer使用の場合
designer  # Qt Designerを起動

# .uiファイルを.pyファイルに変換
pyside6-uic mainwindow.ui -o ui_mainwindow.py

# リソースファイル(.qrc)の変換
pyside6-rcc resources.qrc -o resources_rc.py

# アプリケーション実行
python hello_world.py

# スタンドアロン実行ファイル作成(PyInstaller使用)
pip install pyinstaller
pyinstaller --onefile --windowed hello_world.py

# requirements.txt作成
pip freeze > requirements.txt

プロジェクト構造例

my_pyside6_app/
├── main.py                 # メインアプリケーション
├── ui/
│   ├── __init__.py
│   ├── mainwindow.ui       # Qt Designerファイル
│   └── ui_mainwindow.py    # 変換されたUIファイル
├── resources/
│   ├── icons/
│   ├── images/
│   └── resources.qrc       # リソースファイル
├── models/
│   ├── __init__.py
│   └── data_model.py       # データモデル
├── views/
│   ├── __init__.py
│   └── custom_widgets.py   # カスタムウィジェット
├── utils/
│   ├── __init__.py
│   └── database.py         # データベースユーティリティ
├── requirements.txt
└── README.md