Communityコーディング&開発github.com

mawwki/pyqt6-macos-ui

PyQt6/PySide6でデスクトップGUIアプリを構築、リデザイン、または再スタイルし、macOSアプリのように見せます(フレームレスウィンドウ、信号機ボタン、サイドバー、ソフトシャドウ付きカード、アニメーション、ライト/ダークテーマ)。

pyqt6-macos-ui とは?

このスキルは、PyQt6/PySide6を使用して、デフォルトのQtアプリとは一線を画す、洗練されたmacOSスタイルのデスクトップGUIアプリケーションを構築するための完全な設計仕様を提供します。フレームレスウィンドウ、カスタムの信号機ボタン、サイドバーナビゲーション、ソフトシャドウ付きカード、トグルスイッチ、スライダー、アニメーションなど、macOSアプリに期待されるすべての要素を再現します。特に、Windows、Linux、macOS間で一貫した外観を実現するために、ネイティブAPIに頼らず、Qtのスタイルシート(QSS)とプロパティアニメーションを使用してエステティックを手動で再現するアプローチを採用しています。このスキルには、`app_template.py`というリファレンス実装の正確なビルド仕様(トークン、ウィジェットごとのジオメトリ/タイミング/色、インタラクションロジック、ページセット)と、PyQt6をWindowsで使用する際の既知のクラッシュの落とし穴が含まれています。開発者がこのスキルを参照することで、あたかも専任のUIデザイナーが手掛けたかのような、高品質で一貫性のあるユーザーインターフェースを効率的に作成できます。

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

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

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

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

ドキュメント

PyQt6 を macOS 風に — リファレンスアプリケーションの仕様

以下は、assets/app_template.py を動作としてバイト単位で再現するための完全な仕様です。控えめなパレット、1つのアクセントカラー、ソフトな光、動作を説明するために必要な場所だけに配置されたアニメーションを持つ、macOSスタイルのデスクトップアプリケーションを制作するスタジオのインターフェースデザインエージェントのように振る舞ってください。

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

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


1. クイックスタート

pip install PyQt6
python app_template.py

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

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

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

if __name__ == "__main__":
    main()

2. app_template.py のファイル構造(可読性のために順序が重要)

  1. インポート: sys, time, dataclasses, pathlib.Path; QtCore (Qt, QEvent, QPropertyAnimation, QEasingCurve, QRect( F), QPointF, QParallelAnimationGroup, QVariantAnimation, QTimer, pyqtProperty, pyqtSignal); QtGui (QColor, QFont, QPainter, QFontDatabase, 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。
  4. カスタムウィジェット (依存関係の順に): HoverButton, ToggleSwitch, MacSlider, SegmentedControl, FileDropArea, TrafficLight, SidebarToggle, TitleBar, Sidebar。
  5. build_stylesheet(theme)。
  6. MainWindow + main()。

3. デザイントークン(正確な値)

Theme は、フィールド bg, surface, sidebar, text, text_secondary, accent, separator_rgb (文字列 #000000/#FFFFFF — rgba() のベース), separator_alpha (0..255), shadow_alpha (0..255) を持つ frozen dataclass です。

LIGHT = Theme(bg="#F5F5F7", surface="#FFFFFF", sidebar="#ECECEF", text="#1D1D1F",
              text_secondary="#6E6E73", accent="#1D1D1F",
              separator_rgb="#000000", separator_alpha=20, shadow_alpha=55)
DARK  = Theme(bg="#1E1E1E", surface="#2A2A2C", sidebar="#242426", text="#F5F5F7",
              text_secondary="#98989D", accent="#CFCFD4",
              separator_rgb="#FFFFFF", separator_alpha=20, shadow_alpha=150)

重要:このアプリケーションのアクセントカラーは青ではありません。これは「反転」されています — ライトテーマでは黒 #1D1D1F、ダークテーマでは明るい #CFCFD4 です(これにより、System Blue と同じコントラストを白黒の配色で実現しています)。アクセントカラーの上にテキストを配置するコードは、ハードコードではなく accent_fg(theme) を使用する必要があります。

def accent_fg(theme: Theme) -> str:
    color = QColor(theme.accent)
    luminance = (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255
    return "#1D1D1F" if luminance > 0.6 else "#FFFFFF"

def rgba(hex_color: str, alpha_0_255: int) -> str:
    c = QColor(hex_color)
    return f"rgba({c.red()}, {c.green()}, {c.blue()}, {alpha_0_255})"

定数:

  • RADIUS_WINDOW = 14, RADIUS_CARD = 12, RADIUS_CONTROL = 8, SIDEBAR_WIDTH = 196。
  • EDGE_MARGIN = 6, エッジのビットマスク EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8。
  • DUR_FAST = 140 (hover/press), DUR_MED = 250 (ページ、サイドバー、トグル、セグメント), EASE = QEasingCurve.Type.OutCubic。
  • PAGES = ["Обзор", "Задачи", "Ввод", "Чат", "Файлы", "Настройки"]. (これらも日本語化する必要はなく、コード内での識別子としてそのまま使用されるため、原文のまま保持します)
  • REPLIES — ボットの短い返信5つ。
  • SVG_EYE (Heroicons, viewBox 0 0 24 24, fill="currentColor") と SVG_EYE_SLASH (Bootstrap, viewBox 0 0 16 16) — 複数行の文字列。

フォント: system_font() 内のフォールバックチェーン — Inter → Segoe UI Variable Text → .AppleSystemUIFont → SF Pro Text → Helvetica Neue → Manrope → Segoe UI; さらに setStyleStrategy(QFont.StyleStrategy.PreferAntialias)。

間隔は 4/8/12/16/20/24/32 のスケールから。影はソフト: カード — blurRadius 24, offset (0,5), 色 shadow_color(theme) (黒色に shadow_alpha)。


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) → panel (QFrame, objectName "Panel") → TitleBar (40px) + body (QHBoxLayout) → Sidebar (196px) + separator (1px) + QStackedWidget。 パネルには build_stylesheet(theme) が適用されます — QSSは子に継承されます。

影と角丸は、ウィンドウ自体ではなく子の QFrame#Panel に適用されます: ウィンドウは透明で、影のための余白を提供します。これは基本的なことで、QMainWindow にエフェクトを適用することはできません。

ドラッグとリサイズ

ウィンドウの移動 — Qt6のネイティブメカニズム。手動のオフセット計算は不要:

def mousePressEvent(self, event):   # TitleBar内
    if event.button() == Qt.MouseButton.LeftButton:
        handle = self._window.windowHandle()
        if handle is not None:
            handle.startSystemMove()

端でのサイズ変更 — 同様にネイティブの QWindow.startSystemResize。MainWindow には QApplication 上のイベントフィルターが設定されており、以下の処理を行います:

  • ウィンドウ端から EDGE_MARGIN 以内のゾーンでの MouseButtonPress (Left) 時に _start_system_resize(edges) を呼び出し、True を返します (イベントは消費されます);
  • ボタンなしの MouseMove 時にウィンドウのカーソルを更新します;
  • Leave 時にカーソルをリセットします。

_resize_edges_for(pos) は、グローバル位置をエッジのビットマスクに変換します; ウィンドウが表示されていて最大化されていない場合にのみ機能します。_cursor_for_edges(edges): 中央 → Arrow; 左/右 → SizeHor; 上/下 → SizeVer; 上+左 または 下+右 → SizeFDiag; それ以外 → SizeBDiag。

PyQt6の落とし穴: Qt.Edge は int に変換できず、int と | 演算子で混在させることはできません (TypeError)。したがって、すべてのエッジ計算は独自のビット定数 EDGE_* を使用し、startSystemResize のフラグは次のように組み立てられます:

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. カスタムウィジェット(正確なパラメータ)

HoverButton (スムーズなホバー/プレス効果のあるボタン)

  • QPushButton, setFlat(True), 高さ 34, フォント 13/Medium, padding 0 18px, radius RADIUS_CONTROL。
  • QSS内のテキスト色: color: {accent_fg(theme)} (ハードコードされた「white」ではありません!)。
  • 背景 — QSS :hover (インスタント) ではなく、Q_PROPERTY alpha (float, 0..1): paintEvent 内で アクセント QColor を setAlphaF(alpha) で塗りつぶし + 角丸長方形、その後 super().paintEvent() でテキストを描画します。イベント: enter → 0.85, leave → 1.0, press → 0.7, release → 0.85/1.0。アニメーション DUR_FAST, EASE。

ToggleSwitch (macOSスタイルのトグルスイッチ)

  • QWidget 42×24, シグナル toggled(bool), 左ボタン専用のハンドラ。
  • Q_PROPERTY knobPos (float 0..1) が DUR_MED/EASE でアニメーションします。
  • 描画: トラックは角丸 (radius = h/2)、色は #B9B9BE (オフ) と accent (オン) の間で補間; つまみ — 円形 QColor(self._theme.bg), 直径 h-4, 位置 x_min = 2, x_max = width - knob_d - 2。
  • 落とし穴: つまみは theme.bg で塗りつぶされ、白ではありません — ダークテーマでは、白いつまみが白いオフ状態のトラックと同化します。
  • プログラムによる setChecked はアニメーションのみを行い、シグナルは発行しません (シグナルは mousePressEvent で発行されます)。

MacSlider (macOSスタイルのスライダー)

  • QWidget 高さ 28, Q_PROPERTY knobScale (1.0 → 1.12 hover, 0.94 press), DUR_FAST。
  • xからの値: ratio = (x - pad) / (width - 2*pad), pad = 9.5。
  • 描画: トラック高さ 6 (背景は separator に alpha 26、塗りつぶしは accent)、円形 9.5px: グロー (accent, alpha 22 hover / 34 press, 半径 +4)、影 (黒色 alpha 26)、 円形自体は theme.bg で縁は separator alpha 90 (押下時は 255)、penWidth 1.2。

SegmentedControl (セグメント「コンパクト/ミディアム/ラージ」)

  • QWidget 高さ 30; ボタン高さ 26, フォント 12, padding 0 16px, box spacing 4, margins 4。
  • 「ピル」 — 同じレイアウト内のボタンの後ろにある QFrame#SegIndicator (bg accent, radius 7)。 移動は QPropertyAnimation(geometry) DUR_MED/EASE。
  • テキスト色: 選択されたものは accent_fg(theme), その他は text_secondary。
  • showEvent でピルを下に移動 (lower()) し、選択されたボタンのジオメトリに設定します。

TrafficLight (信号機ボタン)

  • QPushButton 22×22, Q_PROPERTY hover (0..1) アニメーション DUR_MED。
  • 黄/緑の色枠なし: close — 赤色 #FF5F57 (alpha 200 + 35·hover), minimize/zoom — ニュートラル rgba(text_secondary, 60 + 40·hover)。
  • グリフは QPen で RoundCap、幅 2 で描画されます。座標は整数のみ (落とし穴を参照):
    • close — 2本の線による十字;
    • minimize — 下向きシェブロン: (cx-5,cy-3)-(cx,cy+3)-(cx+5,cy-3);
    • maximize (展開されていない状態) — 上向きシェブロン: (cx-5,cy+3)-(cx,cy-3)-(cx+5,cy+3);
    • maximize 展開状態 (_maximized) — 4つの角括弧のような8本の線。
  • グリフの色: close — 白色 alpha 205+30·hover; その他 — hoverに応じて text_secondary → text への補間。

SidebarToggle (サイドバー折りたたみボタン)

  • QPushButton 22×22, hover 0..1, hover-bg rgba(separator,26), グリフは text_secondary → text に補間。
  • 開いたパネルのグリフ (Lucide panel-left-close): 枠 drawRoundedRect(QRectF(cx-7, cy-7, 14, 14), 2, 2), cx-2 に垂直セパレータ、シェブロン < を (cx+4, cy-2)→(cx, cy)→(cx+4, cy+2) で描画。
  • 折りたたまれたグリフ (Lucide panel-right): 枠 + cx+2 にセパレータ、シェブロンなし。
  • set_collapsed(bool) でグリフを切り替え; 状態は MainWindow.toggle_sidebar と同期します。

Sidebar (ナビゲーション)

  • QWidget setFixedWidth(SIDEBAR_WIDTH), margins 10,16,10,16, spacing 2。
  • ブランド 「МойПроект」 (13/Bold, padding: 0 8px 14px 8px)。(ブランド名はそのまま保持)
  • ページ名 — QPushButton#NavButton 高さ 30, フォント 13, checkable, 非アクティブ — text_secondary, アクティブ — text。インジケーター — QFrame 半径 8, bg rgba(accent,40), geometry によって選択されたボタンへアニメーション (DUR_MED)。
  • 現在のページへの非アクティブクリック — 何もしない (no-op)。showEvent では、インジケーターはアニメーションなしで設定されます。

6. 共通QSS (build_stylesheet)

  • #Panel — テーマのbg、ウィンドウの半径。#TitleBar — 透明。#Sidebar — サイドバーのbg。
  • #Indicator — rgba(accent, 40), radius 8。
  • QPushButton#NavButton — 左揃え, radius 8, padding-left 10; :checked — text; :hover — rgba(separator,26)。
  • QPushButton#ToolButton — セカンダリ色, radius 6, hoverハイライト。
  • スクロールバー (チャットやリストが「太く」ならないようにグローバルに追加): 垂直/水平 8px, handle radius 3 rgba(separator,60) (hover 120), min 30, 矢印とページ領域 — 透明。
  • QSS背景を持つすべてのウィジェットに setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) を設定します。

7. ページ (6個)

_page_shell(theme, title) (margins 28/24/28/24, spacing 16, タイトル 22/Bold) と _add_card(theme, layout, title) (カード + タイトル 14/DemiBold + 影) を使用して構成されます。

  1. Обзор (概要) — 「システム」カード (トグル: 自動起動 ✓, 通知 ✓, 省電力 ✗) と「表示」カード (スライダー: 明るさ=70, 音量=45)。スライダーの行 — 左にラベル, 右にコントロール、control_stretch=1 を使用 (stretchの落とし穴を参照)。

  2. Задачи (タスク) — 4つの QCheckBox。インジケーターは 18×18 (border rgba(separator,120), hover → accent, checked → bg accent); プログレスバー (12px, chunk accent), カウンター 「完了 N / M」, QProgressBar のアニメーションは QVariantAnimation 経由 (280ms); ボタン HoverButton("Сбросить")。(ボタンのラベルは「リセット」)

  3. Ввод (入力) — フィールド:

    • 名前、メール — 通常の QLineEdit (34px, radius 8, hover枠 rgba(sep,140), focus → 枠 accent + tint rgba(accent,8), placeholder text_secondary, selection accent+accent_fg)。
    • パスワード — _build_password_field: 末尾のアクション「目」 — アイコンは eye ↔ eye_slash に切り替わり、クリックで EchoMode.Password ↔ Normal を切り替え。
    • 検索 — _build_search_field: ピル半径 17、先頭に拡大鏡アイコン、末尾に バツ印アイコン (テキストが空でない場合のみ表示、クリックでクリア)。
    • 複数行 — QTextEdit (radius 10, placeholder は palette.setColor(QPalette.ColorRole.PlaceholderText, ...) 経由 — QSSセレクタ QTextEdit[placeholderText] は信頼できません) + 右側に文字数カウンター。
    • フィールドのアイコンは _field_icon で描画されます (18px: 虫眼鏡 = 円+ハンドル; バツ印 = 円 rgba(text_secondary,110) + 「✕」を色 theme.bg で) または SVG からレンダリングされます (_svg_icon): レンダリング前にSVG内の currentColor を theme.text_secondary に置き換えます (落とし穴を参照)。
  4. Чат (チャット) — セクション8を参照。

  5. Файлы (ファイル) — セクション9を参照。

  6. Настройки (設定) — 「外観」カード (トグル「ダークテーマ」 → _on_theme_toggled, セグメント「UIサイズ」)、「動作」カード (トグル: 自動更新 ✓, アニメーション ✓)、 コンボボックス「言語」 (QSS: フィールド surface+枠, QAbstractItemView — selection-background-color: accent; selection-color: accent_fg)、スライダー 「アニメーション速度」=50。


8. チャット (ページ「Чат」)

  • コンテナ — 透明な QScrollArea (フレームレス; 白色の背景を避けるため: QSS background: transparent + viewport().setStyleSheet(...transparent) + 内部 QWidget 透明)。
  • メッセージは `self._chat_messages: list[(text, m

...

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

関連スキル