Communitygithub.com

mawwki/pyqt6-md3-ui

PyQt6/PySide6 デスクトップ GUI アプリを、マテリアルデザイン 3 (MD3) スタイルの洗練された外観に構築、再設計、またはスタイル変更します。

pyqt6-md3-ui とは?

このスキルは、Claude Code や Cursor などの AI エージェントが、PyQt6/PySide6 で構築されたデスクトップ GUI アプリケーションの UI を、Google のマテリアルデザイン 3 (Material Design 3) のビジュアルスタイルに完全に準拠させるための詳細な仕様を提供します。フレームレスウィンドウ、カスタムタイトルバー、マテリアルデザインのウィジェット(トグルボタン、スライダー、チェックボックス、セグメントボタン、リップルエフェクト付きボタン、ナビゲーションドロワーなど)、ライト/ダークテーマの切り替えなど、MD3 の設計原則を Qt 上で再現するための具体的な実装手順とコード構造を網羅しています。このスキルは、ユーザーが「Material Design 風」「モダンな UI」「フレームレスウィンドウ」「アニメーション」「ドロワー」「トグル」「スライダー」などの要求をした場合に特に有効であり、提供される `assets/app_template.py` がリファレンス実装となります。エージェントは、この仕様書に従って既存のテンプレートをコピー・アダプトするか、ゼロから新しいアプリを構築することができ、Windows、Linux、macOS 間で一貫した外観を実現します。

対応~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/mawwki/pyqt6-ui-skills/tree/HEAD/pyqt6-md3-ui

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

PyQt6 マテリアルデザイン 3 スタイル — リファレンスアプリケーション仕様

以下は、assets/app_template.py の動作をピクセル単位で再現するための完全な仕様です。MD3 デスクトップアプリケーションを制作する UI デザインスタジオのエージェントのように振る舞ってください。役割パレット(primary / on-primary / containers / surface / outline)、ステートレイヤー、リップル、フォームスケールによる角丸、アクションの発生をユーザーに伝えるための必要最小限のアニメーションを備えています。

最も重要なルール: Qt における「MD3 スタイル」とは、ネイティブ API ではなく、自力で再現した美学です。フレームレスウィンドウ + カスタム QSS + 滑らかさのための QPropertyAnimation で構成されます。これにより、アプリケーションは Windows、Linux、macOS で同じように見えます。ネイティブの Qt スタイルでは、角丸、半透明、ステートレイヤー、アニメーションを実現できません。

リファレンス実装は assets/app_template.py (単一ファイル)です。エージェントのタスクは、このファイルをコピーしてアダプトするか、このドキュメントに従ってゼロから再構築することです。


1. クイックスタート

pip install PyQt6
python app_template.py

ユーザー向けビルド: pyinstaller --windowed --icon=app.ico app_template.py。

アプリケーションは次のように起動します:

def main() -> None:
    app = QApplication(sys.argv)
    app.setStyle("Fusion")                       # QSS を全体で尊重する唯一のスタイル
    app.setFont(system_font(14, QFont.Weight.Normal))
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

2. app_template.py のファイル構成(読みやすさのための順序)

  1. インポート: math, sys, time, dataclasses, pathlib.Path; QtCore (Qt, QEvent, QPropertyAnimation, QEasingCurve, QRect(F), QPointF, QParallelAnimationGroup, QVariantAnimation, QTimer, pyqtProperty, pyqtSignal); QtGui (QColor, QFont, QPainter, QPainterPath, QFontDatabase, QFontMetricsF, QPen, QPixmap, QIcon, QPalette); QtSvg (QSvgRenderer); QtWidgets(必要なすべてのウィジェット)。
  2. デザイントークン: Theme (dataclass), LIGHT/DARK, 半径/持続時間の定数, REPLIES, SVG_EYE/SVG_EYE_SLASH, PAGES。
  3. ヘルパー: system_font, rgba, accent_fg, shadow_color, lerp_color, animate_property, draw_checkmark。
  4. 依存関係の順序に従ったカスタムウィジェット: MdButton, MdSwitch, MdSlider, MdCheckBox, MdSegmentButton/MdSegmentedControl, DropGlyph/FileDropArea, WindowButton, SidebarToggle, TopAppBar, NavButton/NavigationDrawer。
  5. build_stylesheet(theme)。
  6. MainWindow + main()。

3. デザイントークン(正確な値、MD3 ロール)

Theme — frozen dataclass、すべての色は hex 文字列、MD3 ロール: primary, on_primary, primary_container, on_primary_container、 secondary_container, on_secondary_container, surface, surface_container_low、 surface_container, surface_container_high, surface_container_highest, on_surface、 on_surface_variant, outline, outline_variant, error, on_error, error_container、 on_error_container, shadow_alpha (int 0..255、影は常に黒)。

LIGHT = Theme(primary="#6750A4", on_primary="#FFFFFF",
              primary_container="#EADDFF", on_primary_container="#21005D",
              secondary_container="#E8DEF8", on_secondary_container="#1D192B",
              surface="#FEF7FF", surface_container_low="#F7F2FA",
              surface_container="#F3EDF7", surface_container_high="#ECE6F0",
              surface_container_highest="#E6E0E9", on_surface="#1D1B20",
              on_surface_variant="#49454F", outline="#79747E", outline_variant="#CAC4D0",
              error="#B3261E", on_error="#FFFFFF",
              error_container="#F9DEDC", on_error_container="#410E0B", shadow_alpha=45)
DARK  = Theme(primary="#D0BCFF", on_primary="#381E72",
              primary_container="#4F378B", on_primary_container="#EADDFF",
              secondary_container="#4A4458", on_secondary_container="#E8DEF8",
              surface="#141218", surface_container_low="#1D1B20",
              surface_container="#211F26", surface_container_high="#2B2930",
              surface_container_highest="#36343B", on_surface="#E6E0E9",
              on_surface_variant="#CAC4D0", outline="#938F99", outline_variant="#49454F",
              error="#F2B8B5", on_error="#601410",
              error_container="#8C1D18", on_error_container="#F9DEDC", shadow_alpha=120)

「ロール / on-ロール」のペアは必須です: primary 上のテキストは常に on_primary、 secondary_container 上のテキストは on_secondary_container、primary_container 上のテキストは on_primary_container。"white"/"black" のハードコードは禁止です。accent_fg(theme) は compat ヘルパーとして残され、theme.on_primary を返します。

def rgba(hex_color: str, alpha_0_255: int) -> str:   # QSS は 0..255 のアルファ値を期待、0..1 ではない
    c = QColor(hex_color)
    return f"rgba({c.red()}, {c.green()}, {c.blue()}, {alpha_0_255})"

def shadow_color(theme: Theme) -> QColor:
    c = QColor(0, 0, 0); c.setAlpha(theme.shadow_alpha); return c

定数:

  • フォーム: RADIUS_WINDOW = 16, RADIUS_CARD = 12, RADIUS_MENU = 8, RADIUS_FIELD = 4、 SIDEBAR_WIDTH = 220。ボタン/ピル — radius = height/2; チップ/セグメント — full。
  • EDGE_MARGIN = 12 (ルートのシャドウパディングと一致)、エッジのビットマスク EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8。
  • DUR_FAST = 140 (hover), DUR_PRESS = 90, DUR_RELEASE = 200, DUR_MED = 250 (ページ、ドロワー、トグル、セグメント), DUR_RIPPLE = 450; EASE = OutCubic, EASE_PRESS = OutQuad。
  • PAGES = ["Обзор", "Задачи", "Ввод", "Чат", "Файлы", "Настройки"]。
  • REPLIES — 5 つの短いボットの応答。
  • SVG_EYE / SVG_EYE_SLASH — Material Symbols visibility / visibility_off (viewBox 0 0 24 24, fill="currentColor")。

タイポグラフィ: system_font(point_size=14, weight) — チェーン Roboto → Google Sans → Noto Sans → Segoe UI, setStyleStrategy(PreferAntialias)。サイズ: ページタイトル 24、 カードタイトル 16/Medium、ボタンテキスト 14/Medium、本文 14、セカンダリ 12、時刻 11。 余白はスケール 4/8/12/16/24/32 から。

影: ウィンドウ/パネル — blurRadius 24, offset (0,4); カード (elevation 1) — blurRadius 8, offset (0,1); 色 shadow_color(theme)。

ステートレイヤー (コンテナ上、「on-ロール」の色で): hover 0.08, press 0.12 (つまり 0.08*hover + 0.04*press); スライダーは hover 0.10 / press 0.16; チェックボックスは 0.06 (unchecked) / 0.10 (checked)。


4. ウィンドウフレーム

self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setMinimumSize(760, 480)
self.resize(960, 600)
self.setMouseTracking(True)
QApplication.instance().installEventFilter(self)   # エッジリサイズ用

ツリー (_build_ui): QMainWindow → root (QWidget, margins 12) → panel (QFrame#Panel, drop-shadow) → TopAppBar (56px) + body (QHBoxLayout) → NavigationDrawer (220px) + separator (1px, outline_variant) + QStackedWidget。 パネルは build_stylesheet(theme) を受け取ります — QSS は子に継承されます。さらに _apply_palette(theme) は、ネイティブに描画される部分のために QPalette ロール (Window/Base=surface, Text=on_surface, Highlight=primary, HighlightedText=on_primary, PlaceholderText=on_surface_variant, ToolTipBase=surface_container_high…) を設定します。

影と角丸は子の QFrame#Panel に適用され、ウィンドウ自体には適用されません: ウィンドウは透明で、 影のために 12px のパディングを提供します。QMainWindow にエフェクトを適用することはできません。

ドラッグとリサイズ

ウィンドウの移動 — Qt6 のネイティブメカニズム (TopAppBar.mousePressEvent 内):

handle = self._window.windowHandle()
if handle is not None:
    handle.startSystemMove()

TopAppBar のダブルクリック — 最大化/通常。

エッジでのリサイズ — ネイティブ QWindow.startSystemResize。MainWindow には QApplication 上のイベントフィルター が設定されています: MouseButtonPress(Left) が ≤ EDGE_MARGIN のゾーンで発生した場合、_start_system_resize(edges) を呼び出し True を返します; MouseMove でボタンが押されていない場合はカーソルを更新します; Leave でカーソルをリセットします。

_resize_edges_for(pos) → エッジのビットマスク (ウィンドウが表示されていて最大化されていない場合のみ)。 _cursor_for_edges: 中央 → Arrow; 左/右 → SizeHor; 上/下 → SizeVer; 上+左 または 下+右 → SizeFDiag; それ以外 → SizeBDiag。

PyQt6 の落とし穴: Qt.Edge は int に変換できず、| で int と混在できません。 エッジの計算は独自のビット定数 EDGE_* で行われ、フラグは次のように組み立てられます:

flag = Qt.Edge(0)
if edges & EDGE_TOP:    flag |= Qt.Edge.TopEdge
if edges & EDGE_BOTTOM: flag |= Qt.Edge.BottomEdge
if edges & EDGE_LEFT:   flag |= Qt.Edge.LeftEdge
if edges & EDGE_RIGHT:  flag |= Qt.Edge.RightEdge
handle.startSystemResize(flag)

5. カスタムウィジェット(正確なパラメータ)

MdButton (filled / tonal, state layer + ripple)

  • QPushButton, 高さ 40, ピル半径 20, フォント 14/Medium, padding 0 24px, minWidth = text + 48。kind="filled" → bg primary/fg on_primary; kind="tonal" → bg secondary_container/fg on_secondary_container。
  • QSS: background: transparent; color: {fg} — 背景は手動で描画、テキストは super().paintEvent() 経由。
  • Q_PROPERTY hover/press/ripple (float)。fg の色のステートレイヤー: alphaF = 0.08*hover + 0.04*press、角丸矩形でクリップ。
  • Ripple: mousePressEvent でポイントを記憶 (_ripple_origin)、pressed で _ripple_anim を 0→1 (DUR_RIPPLE) で起動; paint ではポイントからの楕円を 半径 max_r*ripple (max_r = 最も遠いコーナーまでの距離 + 4)、alphaF = 0.18*(1-ripple)、 ピル形状でクリップ。アニメーションへの参照は属性に保持します。

MdSwitch (MD3 トグル)

  • QWidget 52×32、シグナル toggled(bool) はクリック時のみ; setChecked は静かにアニメーションします。
  • Q_PROPERTY knobPos (0..1) DUR_MED/EASE、さらに hover/press。
  • Off: トラック surface_container_highest + 枠線 2px outline; つまみ 16px 色 outline、中心 x=16。On: トラック primary 枠線なし; つまみ 24px on_primary、 中心 x=36、内側にチェックマーク primary 色 (draw_checkmark, pen 2px)。Press: つまみ 28px。
  • トラック/つまみは knobPos に応じて lerp_color で補間; 枠線は t→1 で消える (alpha = 255*(1-t)); つまみの周りに 40px のステートレイヤー円。

MdSlider

  • QWidget 高さ 28、valueChanged(int)。トラック 4px radius 2: 非アクティブ部 surface_container_highest、アクティブ部 primary。つまみ — 20px の円 primary + 薄い影 (黒 alpha 30, offset y+1)。ステートレイヤー 40px primary alphaF = 0.10*hover + 0.06*press。
  • x からの値: pad = 10 (つまみの半径)、ratio = (x-pad)/(width-2*pad); ドラッグ時 — 慣性なし、それ以外は fill が DUR_FAST でアニメーション。

MdCheckBox

  • QWidget 高さ 24、フォント 14、minWidth = 34 + text。ボックス 18×18 radius 2 at x=3。
  • Unchecked: 枠線 2px on_surface_variant。Checked: 塗りつぶし primary + チェックマーク on_primary (pen 2px RoundCap/RoundJoin)、チェックマークは段階的に「描画」されます (check 0..1、drawPath による 2 つのセグメント線)。
  • ステートレイヤー 36px: on_surface 0.06 (unchecked) / primary 0.10 (checked) * hover。
  • boxScale (OutBack) は press 時にボックスを縮小; setChecked(checked, animate) は toggled を発行; mouseReleaseEvent でのクリックは、カーソルが rect 内にある場合のみ。

MdSegmentedControl / MdSegmentButton

  • コンテナ高さ 40、ピル半径 20、枠線 1px outline (paintEvent で描画)。
  • セグメントは等幅 (addWidget(btn, 1))、高さ 38、フォント 14/Medium; margins (1,1,1,1), spacing 0。選択されていない隣接セグメント間の区切り線 1px outline (paintEvent で int 座標で描画)。
  • インジケーター — QFrame#SegIndicator (bg secondary_container, radius 19) ボタンの後ろ、 QPropertyAnimation(geometry) DUR_MED; showEvent では — lower() + アニメーションなしのジオメトリ。
  • 選択されたセグメント: テキスト on_secondary_container + テキストの左側にチェックマーク (draw_checkmark); 非選択: on_surface。テキスト色はアニメーション (color QColor property)。セグメントのステートレイヤー — on-ロールの色のピル。

DropGlyph / FileDropArea

  • Glyph 38×38: 下矢印 + トレイ、色 lerp(on_surface_variant, primary, level)、 halo primary alpha 0.10*level。
  • FileDropArea(QFrame): minHeight 150、setAcceptDrops(True)。背景 surface_container_low radius 16; 点線 2px outline_variant (dashPattern [4,3.5]); hover/drag 時 — 点線 primary + オーバーレイ primary_container alpha ≈ 60glow+20press。
  • dragEnterEvent(hasUrls) / dragLeaveEvent / dropEvent (isLocalFile() のみ) → dropped(list[str]); クリック → QFileDialog.getOpenFileNames。すべてのシグナルロジックは不変。

WindowButton (ウィンドウボタン)

  • QPushButton 40×40、radius 20、Q_PROPERTY hover/press、set_maximized(bool)。
  • グリフ QPen 1.8 RoundCap/RoundJoin、int 座標: 最小化 — 水平線; 最大化 — 12×12 の正方形 (outline)、最大化時 — 2 つの重なった正方形; 閉じる — X 印。
  • グリフの色 lerp(on_surface_variant, on_surface, hover); 閉じるは hover 時に背景 error (40px の円) + グリフ on_error。その他は hover 時にステートレイヤー on_surface 0.08。
  • 右側の順序: 最小化、最大化、閉じる。

SidebarToggle

  • QPushButton 40×40、ステートレイヤー on_surface 0.08、グリフ lerp(on_surface_variant, on_surface, hover)。
  • グリフ: 16×16 の枠線 radius 2 + 区切り線; 開いたパネル — 左側の区切り線 + シェブロン < (panel-left-close); 折りたたまれた状態 — 右側の区切り線 (panel-left-open)。
  • set_collapsed(bool); MainWindow.toggle_sidebar と同期。

TopAppBar

  • QWidget#TopAppBar 高さ 56、Panel の上に透明; margins (8,0,8,0), spacing 4。
  • 左に SidebarToggle、次にタイトル「МойПроект」18/Medium on_surface、stretch、右に WindowButton 最小化/最大化/閉じる。_max_btn + _sync_max_state は eventFilter の WindowStateChange を介して。startSystemMove + ダブルクリックで最大化。

NavButton / NavigationDrawer

  • NavButton(QPushButton#NavButton) 高さ 48、フォント 14、checkable; アイコン 20×20 at x=16 は QPainter で描画 (Material Symbols outlined): dashboard (4 つの四角)、tasks (円+チェック)、 edit (鉛筆)、chat (吹き出し)、folder、tune (3 本の線 + ノブ)。テキストは QSS 経由 padding-left: 48px。
  • nextCheckState

関連スキル