pyqt について
PyQt と PySide によるデスクトップアプリ開発を支援するスキルハブです。UI コンポーネント、スレッド処理、テスト、スタイルなど、目的別の Qt ガイドへ案内します。
主な機能
- 017 つの Qt 専門トピックのガイド
- 02MVC と MVVM の設計指針
- 03PyQt と PySide の比較
- 04PyInstaller と cx_Freeze によるパッケージ化
- 05トラブルシューティングと開発の指針
活用シーン
- 01Qt の UI アーキテクチャ選定
- 02シグナル、ウィジェット、ダイアログの情報収集
- 03時間のかかる処理を UI スレッドから分離
- 04Qt アプリのテストやパッケージ化
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ codeatcode/ oss-ai-skills --skill pyqtインストール後、Claude Code で/pyqtを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
multimedia/references/codecs-platforms.mdmedia-pipeline.mdstyling/references/dark-theme.mdthreading/references/advanced-safety.mdlifecycle-cleanup.mdpool-patterns.mdqt-concurrent.mdtesting.mdwidgets/references/event-handling.mditem-views-layouts.md詳細
PyQt/PySide 開発
PyQt と PySide は、クロスプラットフォームのデスクトップアプリを構築するための Qt アプリケーションフレームワークの Python バインディングです。
サブスキル
詳しくは、各専門サブスキルを参照してください。
| スキル | 説明 | パス |
|---|---|---|
| pyqt-core | シグナル、スロット、タイマー、設定、ファイル I/O | core/SKILL.md |
| pyqt-widgets | すべてのウィジェットとレイアウト | widgets/SKILL.md |
| pyqt-threading | QThread、スレッドプール、並行処理 | threading/SKILL.md |
| pyqt-dialogs | 標準ダイアログとカスタムダイアログ | dialogs/SKILL.md |
| pyqt-testing | pytest-qt のテストパターン | testing/SKILL.md |
| pyqt-styling | QSS のスタイル設定とテーマ | styling/SKILL.md |
| pyqt-multimedia | オーディオ、ビデオ、カメラ、録画 | multimedia/SKILL.md |
アーキテクチャの選択:Qt における MVC と MVVM
Qt は複数のアーキテクチャパターンをサポートしています。データ表示の複雑さに応じて選択してください。
MVC(Model-View-Controller)
使用する場面:インライン編集を伴うシンプルなデータ表示、標準のアイテムビュー。
- モデル:
QAbstractItemModelのサブクラスがデータを所有します - ビュー:
QTableView、QTreeView、QListViewがデータを表示します - コントローラー:ビューに組み込まれています(ヘッダーのクリック、選択の処理)
- 検証:モデルの
setData()メソッドで行います - シグナル:モデルが
dataChanged()、rowsInserted()を発行してビューに通知します
# Model owns data and validation
class DataModel(QAbstractTableModel):
def setData(self, index, value, role):
if not self._validate(value):
return False
# update and emit dataChanged
MVVM(Model-View-ViewModel)
使用する場面:異なる形式の同じデータを複数のウィジェットに表示する場合、複雑な UI 状態。
- モデル:データソース(データベース、API、ファイル)
- ビューモデル:モデルのデータを表示用に変換し、UI 状態を管理します
- ビュー:シグナルを介してビューモデルにバインドされた PyQt ウィジェット
- 検証:モデルを更新する前にビューモデルで行います
- シグナル:ビューモデルが
Propertyシグナルを公開し、ビューがそれらに接続します
# ViewModel transforms data for display
class UserViewModel(QObject):
display_name = Property(str, _display_name_changed)
def __init__(self, user_model):
self._user = user_model
self._user.nameChanged.connect(self._on_model_changed)
実践テスト:書式設定はどこに置くべきか?
同じデータを異なる書式で2つのウィジェットに表示する場合、書式設定は両方のウィジェットに重複させず、ビューモデルまたはデリゲートに配置します。
悪い例:
# Widget A
label.setText(f"{value:.2f} €")
# Widget B
label.setText(f"€ {value:,.2f}")
良い例:
# ViewModel provides formatted strings
class PriceViewModel(QObject):
display_eu = Property(str)
display_us = Property(str)
def set_price(self, value):
self._price = value
self.display_eu_changed.emit()
self.display_us_changed.emit()
シグナル/スロット:それだけで十分なのはどんな場合か?
以下の場合は、シグナルとスロットだけで十分です。
- 1つのウィジェットが1つのデータソースを表示する
- 複雑な派生状態がない(例:「Y と Z の場合にのみ X を表示する」)
- 検証をモデルの
setData()に配置できる - ウィジェットなしで UI ロジックをテストする必要がない
以下の場合は、ビューモデル層を追加します。
- 複数のビューで同じデータを異なる形式で表示する必要がある
- UI 状態(有効/無効、表示/非表示)が複雑な条件に依存する
- ウィジェットをインスタンス化せずに表示ロジックをテストしたい
これはどこに属するか?
必要なサブスキルだけを読み込むには、以下の振り分けを使用してください。
| 目的 | 読み込むサブスキル |
|---|---|
シグナル宣言、スロットデコレーター、pyqtSignal/Signal、型付きシグナル、connect()、disconnect()、シグナルチェーン | pyqt/core |
| ウィジェットの構成(複数のウィジェットからカスタムウィジェットを構築すること)、アイテムビュー(QTableView、QTreeView)、デリゲート(QItemDelegate)、イベントフィルター | pyqt/widgets |
QThread のワーカーオブジェクトパターン、QThreadPool/QRunnable、QExecutor、スレッドセーフ、キャンセル、ブロッキング操作 | pyqt/threading |
| 標準ダイアログ(QFileDialog、QMessageBox、QInputDialog、QColorDialog、QFontDialog)、カスタム QDialog のパターン、モーダルとモードレス | pyqt/dialogs |
pytest-qt のフィクスチャ(qtbot)、waitSignal、マウス/キーボードのシミュレーション、ダイアログのテスト、モデル/ビューのテスト | pyqt/testing |
QSS の構文、疑似状態(:hover、:pressed)、ウィジェット固有のスタイル、テーマ設定、プラットフォーム間の違い | pyqt/styling |
Hub に含めない内容
- シグナル/スロットの基礎 →
pyqt/core(この Hub はルーティングのみを行います) - ウィジェット一覧 →
pyqt/widgets(この Hub はルーティングのみを行います) - QSS の構文と例 →
pyqt/styling(この Hub はルーティングのみを行います) - レイアウトのコードスニペット →
pyqt/widgets(この Hub はルーティングのみを行います)
詳細解説
専門的なトピックについては、以下のサブスキルを読み込んでください。
- シグナル/スロットの詳細解説 →
pyqt/core- シグナル宣言、スロットデコレーター、接続、型付きシグナル - ウィジェット、レイアウト、アイテムビュー、イベント処理 →
pyqt/widgets- 表示/入力/コンテナーウィジェット、QVBoxLayout/QHBoxLayout/QGridLayout、イベントフィルター、ショートカット - QSS によるスタイリングとダークテーマ →
pyqt/styling- QSS の構文、疑似状態、ウィジェット固有のスタイル、プロパティベースのスタイリング - ダイアログ →
pyqt/dialogs- 標準ダイアログ(QFileDialog、QMessageBox、QInputDialog)、カスタム QDialog のパターン、モーダル/モードレス - スレッド処理のパターン →
pyqt/threading- QThread のワーカーオブジェクトパターン、QThreadPool/QRunnable、スレッドセーフ、キャンセル - pytest-qt によるテスト →
pyqt/testing- qtbot フィクスチャ、waitSignal、マウス/キーボードのシミュレーション、ダイアログのテスト、モデル/ビューのテスト
PyQt と PySide の比較
| 機能 | PyQt5 | PyQt6 | PySide6 |
|---|---|---|---|
| ライセンス | GPL | GPL | LGPL |
| Qt のバージョン | Qt 5 | Qt 6 | Qt 6 |
| メンテナンス状況 | セキュリティ対応のみ | アクティブ | アクティブ |
| シグナル構文 | pyqtSignal | pyqtSignal | Signal |
| スロット構文 | pyqtSlot | pyqtSlot | Slot |
| プロパティ構文 | pyqtProperty | pyqtProperty | Property |
| 商用利用 | ライセンスが必要 | ライセンスが必要 | 無料 |
| QML 登録 | qmlRegisterType() | qmlRegisterType() | @QmlElement |
それぞれを使う場面
- PySide6:ほとんどのプロジェクトに推奨(LGPL、Qt Company による公式サポート)
- PyQt6:GPL との互換性が必要な場合、または既存の PyQt コードベースがある場合
- PyQt5:レガシープロジェクトのみ(セキュリティ修正のみ)
インストール
PySide6(推奨)
pip install PySide6
PyQt6
pip install PyQt6
PyQt5(レガシー)
pip install PyQt5
追加の依存関係
# System packages (Ubuntu/Debian)
sudo apt install libgl1-mesa-glx libglib2.0-0
# System packages (Fedora)
sudo dnf install mesa-libGL glib2
# System packages (Arch)
sudo pacman -S mesa glib2
基本的なアプリケーション
#!/usr/bin/env python3
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QLabel
from PySide6.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My Application")
self.setGeometry(100, 100, 800, 600)
label = QLabel("Hello, Qt!")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setCentralWidget(label)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
推奨されるプロジェクト構成
my_app/
├── src/
│ ├── __init__.py
│ ├── main.py
│ ├── main_window.py
│ ├── widgets/
│ │ ├── __init__.py
│ │ └── custom_widget.py
│ ├── models/
│ │ └── data_model.py
│ ├── resources/
│ │ ├── icons/
│ │ └── styles/
│ │ └── style.qss
│ └── utils/
│ └── helpers.py
├── tests/
│ └── test_main.py
├── requirements.txt
└── pyproject.toml
クイックリファレンス
コアインポート
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QLabel, QPushButton, QLineEdit, QTextEdit, QComboBox, QSpinBox, QCheckBox, QSlider, QProgressBar, QGroupBox, QTabWidget, QStackedWidget, QSplitter, QListWidget, QTreeWidget, QTableWidget, QScrollArea, QToolBar, QStatusBar
from PySide6.QtCore import Qt, QObject, QTimer, QThread, Signal, Slot, Property, QSize, QPoint, QRect, QSettings, QFile, QDir, QUrl, QMimeData, QDateTime
from PySide6.QtGui import QIcon, QPixmap, QImage, QPainter, QPen, QBrush, QColor, QFont, QCursor, QKeySequence, QShortcut
よく使うプロパティ
widget.setEnabled(True) # Enable/disable
widget.setVisible(False) # Visibility
widget.setToolTip("Help") # Tooltip
widget.setObjectName("myButton") # QSS selector
widget.setProperty("primary", True) # Custom property
パッケージ化と配布
PyInstaller
# Install
pip install pyinstaller
# Single executable
pyinstaller --onefile --windowed app.py
# With icon and data files
pyinstaller --onefile --windowed --icon=app.ico --add-data "resources:resources" app.py
PyInstaller の Spec ファイル
# app.spec
a = Analysis(
['app.py'],
pathex=[],
binaries=[],
datas=[('resources', 'resources')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
icon='app.ico',
)
cx_Freeze
# setup.py
from cx_Freeze import setup, Executable
build_options = {
'packages': ['PySide6'],
'excludes': ['tkinter'],
'include_files': [('resources', 'resources')]
}
setup(
name='MyApp',
version='1.0',
description='My Qt Application',
options={'build_exe': build_options},
executables=[Executable('app.py', base='Win32GUI', icon='app.ico')]
)
ビルドコマンド
# PyInstaller
pyinstaller --clean app.spec
# cx_Freeze
python setup.py build
# Create distribution
pyinstaller --clean --distpath dist app.spec
配布チェックリスト
- クリーンな VM(Python がインストールされていない環境)でテストする
- すべてのアセット(アイコン、QSS、翻訳)が同梱されていることを確認する
- 実行ファイルのサイズを確認する(必要に応じてデバッグシンボルを削除する)
- 対象 OS のバージョンでテストする
- 実行ファイルに署名する(Windows/macOS)
- インストーラーを作成する(任意:NSIS、Inno Setup、dmgbuild)
ベストプラクティス
アーキテクチャ
- UI とビジネスロジックを分離する - MVC または MVVM パターンを使用する
- 依存性注入を使用する - 依存関係をコンストラクターに渡す
- ウィジェットをステートレスに保つ - 状態はウィジェットではなくモデルに保存する
- シグナルを使って疎結合にする - コンポーネント間の通信にはシグナルを使用する
- 重いリソースは遅延読み込みする - 必要に応じて画像やデータを読み込む
パフォーマンス
- メインスレッドをブロックしない - 時間のかかる処理にはワーカーを使用する
- 複雑なアイテム描画にはビューのデリゲートを使用する - 不要に QItemDelegate をサブクラス化しない
- UI の更新をまとめて行う - 一括変更中はシグナルをブロックする
- ウィジェットを再利用する - 頻繁に作成するウィジェットはプールする
- 最適化の前にプロファイリングする -
python -m cProfileを使用する
メモリ管理
- 親子関係を利用する - Qt が子オブジェクトを自動的にクリーンアップします
- 動的なウィジェットには
deleteLater()を呼び出す -delを直接使用しない - シグナル/スロットの接続を解除する - 削除する前に接続を解除する
- 循環参照を避ける - 必要に応じて弱参照を使用する
tracemallocで監視する - メモリーリークを見つける
コードの構成
- 1ファイルに1クラス - モジュールの役割を絞ります
- 関連するウィジェットをまとめる - カスタムウィジェットは個別のモジュールに分けます
- 定数を一元管理する - 固定値には列挙型を使用します
- 型ヒントを使用する - IDE のサポートやデバッグに役立ちます
- 公開 API をドキュメント化する - メソッドやクラスに docstring を記述します
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| UI がフリーズする | メインスレッドでブロッキング処理を実行している | ワーカースレッドに移動します |
| ウィジェットへのアクセス時にクラッシュする | ワーカースレッドから UI にアクセスしている | 代わりにシグナルを使用します |
| メモリリークが発生する | オブジェクトがクリーンアップされていない | 親子関係を使用し、deleteLater() を呼び出します |
| QSS が適用されない | セレクターまたは構文が間違っている | ウィジェットの objectName を確認し、style().polish() を使用します |
| シグナルが発火しない | 接続の種類が間違っている | シグナルの宣言を確認し、スレッドアフィニティを確認します |
| CPU 使用率が高い | メインスレッドでループが高速に回り続けている | タイマーまたはワーカーを使用します |
| ウィンドウが表示されない | show() の呼び出しがない | app.exec() の前に window.show() を呼び出します |
| アイコンが読み込まれない | パスまたは形式が間違っている | バンドルされたアセットには QResource を使用します |
デバッグのヒント
# Enable Qt logging
import logging
logging.basicConfig(level=logging.DEBUG)
# Check for unhandled exceptions
import sys
sys.excepthook = lambda exc: print(f"Unhandled: {exc}")
# Print widget hierarchy
print(window.findChildren(QWidget))
# Check signal connections
print(button.receivers(button.clicked))
# Dump QSS errors
app.setStyleSheet("INVALID {") # Will print parse errors
参考資料
コメント
Markdown 対応