Community编程与开发github.com

mawwki/pyqt6-macos-ui

构建、重新设计或重新样式化 PyQt6/PySide6 桌面 GUI 应用程序,使其看起来和感觉上像是经过打磨的 macOS 原生应用(无边框窗口、交通灯按钮、侧边栏、带柔和阴影的卡片、动画微交互、浅色/深色主题)。适用于当用户要求制作“美观/时尚”、“macOS/Mac 风格”、“现代界面”、“无边框窗口”、“像 macOS 一样”的桌面 UI,或引用 `assets/app_template.py` 外观时使用。包含参考应用 `app_template.py` 的完整构建规范(设计令牌、每个小部件的几何/时间/颜色、交互逻辑、页面集)以及 PyQt6 在 Windows 上崩溃的必知陷阱。在编写任何 PyQt6/PySide6 UI 代码前请先查阅此技能,以避免结果看起来像默认的 Qt 应用。

pyqt6-macos-ui 是什么?

此技能提供了构建具有 macOS 原生外观和感觉的 PyQt6/PySide6 桌面应用的完整规范和指南。它围绕一个名为 `app_template.py` 的参考实现进行组织,该实现演示了如何通过无边框窗口、自定义 QSS 样式表和 QPropertyAnimation 动画来重现 macOS 美学,而不依赖于任何特定平台的 API。这确保了应用在 Windows、Linux 和 macOS 上具有一致的外观。 该技能详细说明了精确的设计令牌(主题、颜色、圆角半径、动画持续时间)、自定义小部件(如交通灯按钮、开关滑块、macOS 风格的滑块和分段控件)以及六种页面类型(概览、任务、输入、聊天、文件、设置)的完整布局和交互逻辑。它还涵盖了关键功能,如原生窗口移动和调整大小、侧边栏导航、聊天界面、文件拖放和密码字段。最重要的是,它明确指出了 PyQt6 在 Windows 上常见的崩溃陷阱,例如 `Qt.Edge` 枚举与整数运算时的类型错误以及 QSS 背景属性设置要求。通过使用此技能,开发人员可以快速创建看起来不像默认 Qt 应用的、高度打磨的桌面 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 中提问

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

文档

PyQt6 在风格 macOS 上——参考应用规范

以下是完整的规范,可用于按行为逐字节地重现 assets/app_template.py。模拟一个来自工作室的界面设计代理,该工作室制作 macOS 风格的桌面应用程序:内敛的调色板、单一的强调色、柔和的光线、动画恰如其分地解释发生了什么——仅此而已。

黄金法则:Qt 中的“macOS 风格”不是原生 API,而是手动重现的美学:无边框窗口 + 自定义 QSS + QPropertyAnimation 实现流畅性。这样,应用程序在 Windows、Linux 和 macOS 上看起来都一样。Qt 原生样式无法提供圆角、半透明和动画。

参考实现是 assets/app_template.py(单个文件)。代理的任务是:要么复制并适配它,要么根据本文档从头开始重建。


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 是一个 frozen dataclass,包含字段:bg、surface、sidebar、text、text_secondary、accent、separator_rgb(字符串 #000000/#FFFFFF——rgba() 的基础)、separator_alpha(0..255)、shadow_alpha(0..255)。

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(这提供了与系统蓝相同的对比度,但采用黑白配色方案)。任何将文本放置在强调色背景上的代码必须使用 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 的事件过滤器**,它:

  • 在 MouseButtonPress(左键)事件中,如果位置在离窗口边缘 ≤ EDGE_MARGIN 的区域内,则调用 _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_CONTROL。
  • QSS 中的文本:color: {accent_fg(theme)}(不是硬编码的 "white"!)。
  • 背景——不是 QSS :hover(瞬间的),而是 Q_PROPERTY alpha(float,0..1):在 paintEvent 中,使用 setAlphaF(alpha) + 圆角矩形绘制强调色 QColor,然后调用 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 悬停、0.94 按下)、DUR_FAST。
  • 从 x 获取值:ratio = (x - pad) / (width - 2*pad)、pad = 9.5。
  • 绘制:轨道高度 6(背景使用 separator 的 alpha 26,填充使用 accent)、圆形 9.5px:发光(accent、悬停时 alpha 22 / 按下时 34、半径 +4)、阴影(黑色 alpha 26)、圆形本身为 theme.bg 并带有边框 separator alpha 90(按下时为 255)、penWidth 1.2。

SegmentedControl(分段控件“紧凑/中等/大”)

  • QWidget 高度 30;按钮高度 26、字体 12、padding 0 16px、盒子 spacing 4、边距 4。
  • “药丸”——QFrame#SegIndicator(背景 accent、圆角半径 7)位于按钮后面的同一布局中,使用 QPropertyAnimation(geometry) DUR_MED/EASE 移动。
  • 文本颜色:选中的按钮使用 accent_fg(theme),其余的按钮使用 text_secondary。
  • 在 showEvent 中,将药丸向下放置(lower())并设置其几何形状为选中按钮的几何形状。

TrafficLight(交通灯按钮)

  • QPushButton 22×22、Q_PROPERTY hover(0..1)动画 DUR_MED。
  • 没有黄色/绿色的彩色边框:关闭——红色 #FF5F57(alpha 200 + 35·hover)、最小化/最大化——中性色 rgba(text_secondary, 60 + 40·hover)。
  • 使用 QPen 和 RoundCap 绘制字形、宽度 2、仅整数坐标(参见陷阱):
    • 关闭——两条线组成的十字架;
    • 最小化——向下的 V 形:(cx-5,cy-3)-(cx,cy+3)-(cx+5,cy-3);
    • 最大化(非展开状态)——向上的 V 形:(cx-5,cy+3)-(cx,cy-3)-(cx+5,cy+3);
    • 最大化在展开状态下(_maximized)——8 条线组成四个角括号。
  • 字形颜色:关闭——白色 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 处绘制垂直分隔线、< 形 V 从 (cx+4, cy-2)→(cx, cy)→(cx+4, cy+2)。
  • 折叠状态的字形(Lucide panel-right):边框 + 在 cx+2 处的分隔线、无 V 形。
  • set_collapsed(bool) 切换字形;状态与 MainWindow.toggle_sidebar 同步。

Sidebar(侧边栏导航)

  • QWidget setFixedWidth(SIDEBAR_WIDTH)、边距 10,16,10,16、间距 2。
  • 品牌“我的项目”(13/Bold、padding: 0 8px 14px 8px)。
  • 页面名称——QPushButton#NavButton 高度 30、字体 13、checkable、非激活状态——text_secondary、激活状态——text。指示器——QFrame 圆角半径 8、背景 rgba(accent,40)、根据 geometry 动画到选中的按钮(DUR_MED)。
  • 点击当前页面的非激活状态——无操作。在 showEvent 中,指示器无动画放置。

6. 全局 QSS(build_stylesheet)

  • #Panel —— 主题背景、窗口圆角半径。#TitleBar —— 透明。#Sidebar —— 侧边栏背景。
  • #Indicator —— rgba(accent, 40)、圆角半径 8。
  • QPushButton#NavButton —— 左对齐、圆角半径 8、padding-left 10;:checked —— text;:hover —— rgba(separator,26)。
  • QPushButton#ToolButton —— 次要颜色、圆角半径 6、悬停时高亮。
  • 滚动条(全局添加,以确保聊天和列表不会显得“厚重”):垂直/水平 8px、手柄圆角半径 3 rgba(separator,60)(悬停时 120)、最小 30、箭头和页面——透明。
  • 在任何具有 QSS 背景的控件上设置 setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)。

7. 页面(共 6 个)

通过 _page_shell(theme, title)(边距 28/24/28/24、间距 16、标题 22/Bold)和 _add_card(theme, layout, title)(卡片 + 标题 14/DemiBold + 阴影)进行组合。

  1. 概览 —— “系统”卡片(开关:自动启动 ✓、通知 ✓、省电模式 ✗)和“显示”卡片(滑块:亮度=70、音量=45)。滑块行 —— 左侧标签、右侧控件并设置 control_stretch=1(参见关于 stretch 的陷阱)。

  2. 任务 —— 4 个 QCheckBox,指示器 18×18(边框 rgba(separator,120)、悬停时 → accent、选中时 → 背景 accent);进度条(12px、块 accent)、计数器“已完成 N 项,共 M 项”、通过 QVariantAnimation 实现 QProgressBar 动画(280ms);HoverButton("重置") 按钮。

  3. 输入 —— 字段:

    • 姓名、电子邮件 —— 常规 QLineEdit(34px、圆角半径 8、悬停边框 rgba(sep,140)、焦点 → 边框 accent + 色调 rgba(accent,8)、占位符 text_secondary、选中状态 accent+accent_fg)。
    • 密码 —— _build_password_field:尾部操作“眼睛”——图标在 eye ↔ eye_slash 之间切换、点击切换 EchoMode.Password ↔ Normal。
    • 搜索 —— _build_search_field:药丸圆角半径 17、前导图标放大镜、尾部图标叉号、仅在文本非空时可见(点击 —— 清除)。
    • 多行 —— QTextEdit(圆角半径 10、通过 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、分段“界面大小”)、“行为”卡片(开关:自动更新 ✓、动画 ✓)、组合框“界面语言”(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.

相关技能