Claude-skill-registry-data lorairo-qt-widget
PySide6 widget implementation for LoRAIro GUI with Signal/Slot pattern, Direct Widget Communication, and Qt Designer integration best practices
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry-data
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/lorairo-qt-widget" ~/.claude/skills/majiayu000-claude-skill-registry-data-lorairo-qt-widget && rm -rf "$T"
manifest:
data/lorairo-qt-widget/SKILL.mdsource content
LoRAIro Qt Widget Skill
このSkillは、LoRAIroプロジェクトにおけるPySide6ウィジェット実装のベストプラクティスを提供します。
使用タイミング
- 新しいGUIウィジェットの実装
- 既存ウィジェットのリファクタリング
- Signal/Slot接続の実装
- Qt Designerファイル統合
LoRAIroの Widget実装パターン
基本構造
from PySide6.QtWidgets import QWidget from PySide6.QtCore import Signal, Slot from typing import Optional from loguru import logger class ExampleWidget(QWidget): """サンプルウィジェット Signals: data_changed: データ変更時に発火(新しいデータを送信) action_requested: ユーザーアクション時に発火 """ # 型安全なシグナル定義 data_changed = Signal(str) # str型のデータを送信 action_requested = Signal() # パラメータなし def __init__(self, parent: Optional[QWidget] = None): super().__init__(parent) self._setup_ui() self._connect_signals() self._data: Optional[str] = None def _setup_ui(self) -> None: """UI初期化""" # Qt Designerファイル使用時: # from .ExampleWidget_ui import Ui_ExampleWidget # self._ui = Ui_ExampleWidget() # self._ui.setupUi(self) # 手動レイアウト時: layout = QVBoxLayout() self.setLayout(layout) def _connect_signals(self) -> None: """内部Signal/Slot接続""" # self._ui.button.clicked.connect(self._on_button_clicked) pass @Slot(str) def set_data(self, data: str) -> None: """データ設定(外部から呼び出し可能)""" if self._data != data: self._data = data self._update_display() self.data_changed.emit(data) def _update_display(self) -> None: """表示更新(内部処理)""" # UI更新ロジック pass @Slot() def _on_button_clicked(self) -> None: """ボタンクリックハンドラ(プライベート)""" logger.debug("Button clicked") self.action_requested.emit()
重要な実装パターン
1. Direct Widget Communication
# LoRAIroの推奨パターン: Widget間直接接続 class ThumbnailWidget(QWidget): """サムネイル表示ウィジェット""" image_metadata_selected = Signal(dict) # メタデータを直接送信 def _on_thumbnail_clicked(self, index: int) -> None: """サムネイルクリック時""" metadata = self._image_metadata[index] self.image_metadata_selected.emit(metadata) # 直接送信 class ImageDetailsWidget(QWidget): """画像詳細表示ウィジェット""" def connect_to_thumbnail_widget(self, thumbnail_widget: ThumbnailWidget) -> None: """サムネイルウィジェットに直接接続""" thumbnail_widget.image_metadata_selected.connect( self._on_metadata_received ) @Slot(dict) def _on_metadata_received(self, metadata: dict) -> None: """メタデータ受信時""" self._display_metadata(metadata) # MainWindowでの接続 class MainWindow(QMainWindow): def __init__(self): super().__init__() self._setup_widgets() self._connect_widgets() def _connect_widgets(self) -> None: """Widget間接続(一箇所に集約)""" self.image_details.connect_to_thumbnail_widget(self.thumbnail)
2. 型安全なSignal定義
# ✅ Good: 型指定付きSignal class DataWidget(QWidget): # 単一パラメータ data_changed = Signal(str) score_updated = Signal(float) count_changed = Signal(int) # 複数パラメータ item_selected = Signal(str, int) # (name, index) # 複雑なデータは dict metadata_loaded = Signal(dict) # ❌ Bad: 型なしSignal class BadWidget(QWidget): data_changed = Signal() # 何が送信されるか不明 value_updated = Signal(object) # 型が不明確
3. Qt Designerファイル統合
# UI生成コマンド # uv run python scripts/generate_ui.py from .ExampleWidget_ui import Ui_ExampleWidget class ExampleWidget(QWidget): def __init__(self, parent: Optional[QWidget] = None): super().__init__(parent) # Qt Designer UIのロード self._ui = Ui_ExampleWidget() self._ui.setupUi(self) # UI要素への接続 self._ui.okButton.clicked.connect(self._on_ok_clicked) self._ui.inputField.textChanged.connect(self._on_text_changed) @Slot() def _on_ok_clicked(self) -> None: text = self._ui.inputField.text() logger.debug(f"OK clicked with text: {text}")
4. 非同期処理との統合
from PySide6.QtCore import QThreadPool from src.lorairo.gui.workers.base import LoRAIroWorkerBase class AsyncWidget(QWidget): """非同期処理を扱うウィジェット""" def __init__(self): super().__init__() self._worker_manager = None # WorkerManagerへの参照 def set_worker_manager(self, worker_manager) -> None: """WorkerManager設定""" self._worker_manager = worker_manager def start_async_operation(self) -> None: """非同期操作開始""" worker = MyAsyncWorker(data=self._data) worker.signals.progress.connect(self._on_progress) worker.signals.finished.connect(self._on_finished) worker.signals.error.connect(self._on_error) self._worker_manager.submit(worker) @Slot(int, int) def _on_progress(self, current: int, total: int) -> None: """進捗更新""" progress = int(current / total * 100) self._ui.progressBar.setValue(progress) @Slot(object) def _on_finished(self, result) -> None: """完了処理""" logger.info(f"Operation finished: {result}") self._display_result(result) @Slot(str) def _on_error(self, error_msg: str) -> None: """エラー処理""" logger.error(f"Operation error: {error_msg}") QMessageBox.warning(self, "Error", error_msg)
LoRAIro固有のガイドライン
ファイル配置
- Widgets:
src/lorairo/gui/widgets/ - Designer files:
src/lorairo/gui/designer/*.ui - Generated UI:
(自動生成)src/lorairo/gui/designer/*_ui.py - Window:
src/lorairo/gui/window/main_window.py
命名規則
- Class:
(例:{Name}Widget
,ThumbnailWidget
)ImageDetailsWidget - Signals:
(例:{action}_{tense}
,data_changed
)item_selected - Public methods:
,set_*
,get_*update_* - Private methods:
(イベントハンドラ),_on_*
(内部処理)_update_* - Slots:
デコレータ必須@Slot()
Direct Widget Communication原則
- 中間レイヤー回避: DatasetStateManager経由を避け、直接接続
- MainWindowで集約:
メソッドに全接続を集約_connect_widgets() - connect_to_ メソッド*: 各Widgetに
メソッド提供connect_to_{other_widget}() - 型安全: Signal/Slotは型指定必須
ベストプラクティス
DO ✅
- @Slot デコレータ: 全Slotに
必須@Slot(型) - 型ヒント: 全メソッドに型ヒント
- private/public分離:
プレフィックスで明確化_ - connect_to_ パターン*: Widget間接続メソッド提供
- ログ記録: 重要なイベントはloguru でログ
DON'T ❌
- 直接UI操作: 外部から
は禁止widget._ui.button - グローバル状態: Widget内で共有状態を持たない
- ビジネスロジック混入: Widgetは表示とイベント処理のみ
- 長時間処理: UI スレッドで重い処理を避ける(Worker使用)
- 暗黙的接続: 自動接続に期待せず、明示的に
connect()
テスト戦略
import pytest from PySide6.QtWidgets import QApplication from src.lorairo.gui.widgets.example_widget import ExampleWidget @pytest.fixture def app(qtbot): """Qt application fixture""" return QApplication.instance() or QApplication([]) @pytest.fixture def widget(qtbot): """Widget fixture""" w = ExampleWidget() qtbot.addWidget(w) return w def test_signal_emission(qtbot, widget): """Signal発火テスト""" with qtbot.waitSignal(widget.data_changed, timeout=1000) as blocker: widget.set_data("test data") assert blocker.args[0] == "test data" def test_button_click(qtbot, widget): """ボタンクリックテスト""" with qtbot.waitSignal(widget.action_requested): qtbot.mouseClick(widget._ui.button, Qt.LeftButton)
参考リソース
- 既存Widget:
src/lorairo/gui/widgets/ - MainWindow:
src/lorairo/gui/window/main_window.py - Worker Base:
src/lorairo/gui/workers/base.py - テスト例:
tests/gui/widgets/