Communitygithub.com

mawwki/pyqt6-md3-ui

构建、重新设计或重新样式化 PyQt6/PySide6 桌面 GUI 应用程序,使其外观和体验如 polished Material Design 3 (MD3) 应用一般(无边框窗口、Material 窗口按钮、带指示器的导航抽屉、浮起卡片、带涟漪的色调/填充按钮、MD3 开关/滑块/复选框/分段按钮、浅色/深色主题)。

pyqt6-md3-ui 是什么?

该工具/技能旨在帮助开发者使用 PyQt6/PySide6 快速构建遵循 Material Design 3 (MD3) 规范的桌面 GUI 应用。它通过提供一套完整的自定义组件库(如 MdButton、MdSwitch、MdSlider 等)和设计系统,使得开发者无需深入理解 Qt 底层样式即可复现 MD3 的视觉和交互效果,包括无边框窗口、手绘状态层、涟漪动画、自适应亮暗主题等。该技能支持 Windows、Linux 和 macOS 平台,其核心思路是使用 Fusion 样式并配合手动绘制的部件,以规避不同平台原生样式的不一致性。它包含一个完整的参考实现文件 `app_template.py`,开发者可直接复制、适配或从头重建。使用时,通过 `pip install PyQt6` 即可开始,并可通过 PyInstaller 打包为独立可执行文件。该技能特别适合需要打造现代化、高品质桌面 UI 的场景,例如个人工具、企业内部应用或演示原型。

兼容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/mawwki/pyqt6-ui-skills/tree/HEAD/pyqt6-md3-ui

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

PyQt6 风格 Material Design 3 — 参考应用规格说明

以下是完整规格,可通过行为逐字节复现 assets/app_template.py。模拟一个来自工作室的界面设计代理,该工作室制作 MD3 桌面应用:角色调色板(主色 / on-primary / 容器 / 表面 / 轮廓)、状态层、涟漪、按形状比例圆角、仅在需要解释发生了什么时才有的动画——仅此而已。

主要规则: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。不能硬编码“白色”/“黑色”。accent_fg(theme) 作为兼容辅助函数保留,返回 theme.on_primary。

def rgba(hex_color: str, alpha_0_255: int) -> str:   # QSS 需要 alpha 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;芯片/分段——完整。
  • EDGE_MARGIN = 12(与根边距下的阴影一致),边缘位掩码 EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8。
  • DUR_FAST = 140(悬停),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);卡片(高程 1)— blurRadius 8,offset (0,1);颜色 shadow_color(theme)。

状态层(在容器上方,使用“on-角色”颜色):悬停 0.08,按下 0.12(即 0.08*hover + 0.04*press);滑块悬停 0.10 / 按下 0.16;复选框 0.06(未选中)/ 0.10(选中)。


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。
  • 轨道/旋钮通过 lerp_color 按 knobPos 插值;轮廓在 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。
  • 未选中:轮廓 2px on_surface_variant。选中:填充 primary + 勾号 on_primary (pen 2px RoundCap/RoundJoin),勾号逐步“绘制”(check 0..1,两条分段线通过 drawPath)。
  • 状态层 36px:on_surface 0.06 (unchecked) / primary 0.10 (checked) * hover。
  • boxScale (OutBack) 在按下时压缩盒子;setChecked(checked, animate) 发射 toggled;在 mouseReleaseEvent 中点击仅当光标在矩形内时触发。

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),光晕 primary alpha 0.10*level。
  • FileDropArea(QFrame):minHeight 150,setAcceptDrops(True)。背景 surface_container_low radius 16;虚线 2px outline_variant (dashPattern [4,3.5]);悬停/拖拽时 — 虚线 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),展开时 — 两个重叠正方形;关闭 — 叉号 X。
  • 字形颜色 lerp(on_surface_variant, on_surface, hover);关闭时悬停 — 背景 error (圆 40px) + 字形 on_error。其他悬停时 — 状态层 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 squares),tasks (circle+check),edit (pencil),chat (bubble),folder,tune (3 lines + knobs)。文本通过 QSS padding-left: 48px。
  • nextCheckState

相关技能