# Lorairo Qt Widget

> PySide6 widget implementation for LoRAIro GUI with Signal/Slot pattern, Direct Widget Communication, and Qt Designer integration best practices

- Skill: `majiayu000/lorairo-qt-widget` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/lorairo-qt-widget`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/lorairo-qt-widget/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/lorairo-qt-widget

---


# 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/`

