Claude Code Plugins

Community-maintained marketplace

Feedback

lorairo-qt-widget

@NEXTAltair/LoRAIro
0
0

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

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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実装パターン

基本構造

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原則

  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()

テスト戦略

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/