PySide6 widget implementation for LoRAIro GUI with Signal/Slot pattern, Direct Widget Communication, and Qt Designer integration best practices
Scanned 2/12/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
name: lorairo-qt-widget
description: PySide6 widget implementation for LoRAIro GUI with Signal/Slot pattern, Direct Widget Communication, and Qt Designer integration best practices
allowed-tools: mcp__serena__find_symbol, mcp__serena__get_symbols_overview, mcp__serena__read_memory, Read, Edit, Write, Bash
---
# LoRAIro Qt Widget Skill
このSkillは、LoRAIroプロジェクトにおけるPySide6ウィジェット実装のベストプラクティスを提供します。
## 使用タイミング
- 新しいGUIウィジェットの実装
- 既存ウィジェットのリファクタリング
- Signal/Slot接続の実装
- Qt Designerファイル統合
## LoRAIroの Widget実装パターン
### 基本構造
```python
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
```python
# 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定義
```python
# ✅ 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ファイル統合
```python
# 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. 非同期処理との統合
```python
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原則
1. **中間レイヤー回避**: DatasetStateManager経由を避け、直接接続
2. **MainWindowで集約**: `_connect_widgets()` メソッドに全接続を集約
3. **connect_to_* メソッド**: 各Widgetに `connect_to_{other_widget}()` メソッド提供
4. **型安全**: Signal/Slotは型指定必須
## ベストプラクティス
### DO ✅
- **@Slot デコレータ**: 全Slotに `@Slot(型)` 必須
- **型ヒント**: 全メソッドに型ヒント
- **private/public分離**: `_` プレフィックスで明確化
- **connect_to_* パターン**: Widget間接続メソッド提供
- **ログ記録**: 重要なイベントはloguru でログ
### DON'T ❌
- **直接UI操作**: 外部から `widget._ui.button` は禁止
- **グローバル状態**: Widget内で共有状態を持たない
- **ビジネスロジック混入**: Widgetは表示とイベント処理のみ
- **長時間処理**: UI スレッドで重い処理を避ける(Worker使用)
- **暗黙的接続**: 自動接続に期待せず、明示的に `connect()`
## テスト戦略
```python
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/`
No comments yet. Be the first to comment!