PyQt6 在 macOS 風格 — 參考應用程式規格
以下是完整的規格,可依行為逐位元組重現 assets/app_template.py。模擬一位來自專注於打造 macOS 風格桌面應用程式工作室的介面設計師代理:內斂的調色盤、一個強調色、柔和的光線、動畫僅在解釋發生了什麼時出現 — 僅此而已。
主要規則:Qt 中的「macOS 風格」並非原生的 API,而是手動重現的美學:
frameless 視窗 + 自訂 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 檔案結構(為可讀性,順序重要)
- 導入:
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(所有需要的 widget)。 - 設計代號:
Theme(dataclass)、LIGHT/DARK、半徑/時長常數、REPLIES、SVG_EYE/SVG_EYE_SLASH、PAGES。 - 輔助函式:
system_font、rgba、accent_fg、shadow_color。 - 自訂 widget(依賴順序):
HoverButton、ToggleSwitch、MacSlider、SegmentedControl、FileDropArea、TrafficLight、SidebarToggle、TitleBar、Sidebar。 build_stylesheet(theme)。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 的事件過濾器,它會:
- 在距離視窗邊緣 ≤
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. 自訂 Widget(精確參數)
HoverButton(具有平滑 hover/press 效果的按鈕)
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 開關)
QWidget42×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、hover 時 alpha 22 / press 時 alpha 34、半徑 +4)、陰影(黑色 alpha 26)、 圓形本身為theme.bg並帶有separator邊框(alpha 90,pressed 時為 255)、penWidth 1.2。
SegmentedControl(「緊湊/中等/大型」區段)
QWidget高度 30;按鈕高度 26、字型 12、padding 0 16px、盒子spacing 4、margins 4。- 「藥丸」指示器 —
QFrame#SegIndicator(bgaccent、圓角 7) 位於按鈕後方,佈局相同, 透過QPropertyAnimation(geometry)以DUR_MED/EASE移動。 - 文字顏色:所選按鈕為
accent_fg(theme)、其餘為text_secondary。 - 在
showEvent中,將藥丸指示器置底 (lower()) 並設定為所選按鈕的幾何形狀。
TrafficLight(交通燈號)
QPushButton22×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(側邊欄摺疊按鈕)
QPushButton22×22、hover0..1、hover 背景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)的 V 形<。 - 摺疊狀態的圖示 (Lucide
panel-right):邊框 + 位於cx+2的分隔線、沒有 V 形。 set_collapsed(bool)切換圖示;狀態由MainWindow.toggle_sidebar同步。
Sidebar(導航欄)
QWidgetsetFixedWidth(SIDEBAR_WIDTH)、margins10,16,10,16、spacing 2。- 品牌「我的專案」(13/Bold、
padding: 0 8px 14px 8px)。 - 頁面名稱 —
QPushButton#NavButton高度 30、字型 13、checkable、 非啟用狀態 —text_secondary、啟用狀態 —text。指示器 —QFrame圓角 8、 bgrgba(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、hover 時高亮。- 捲動條(全域添加,防止聊天和列表看起來「厚重」):
垂直/水平 8px、把手圓角 3
rgba(separator,60)(hover 120)、最小值 30、 箭頭和頁面 — 透明。 - 在任何具有 QSS 背景的 widget 上設定
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 + 陰影)組合。
-
總覽 — 「系統」卡片(切換開關:自動啟動 ✓、通知 ✓、省電模式 ✗) 和「顯示」卡片(滑桿:亮度=70、音量=45)。滑桿行 — 標籤在左側、 控制項在右側,
control_stretch=1(請參閱關於 stretch 的陷阱)。 -
任務 — 4 個
QCheckBox,指示器 18×18 (borderrgba(separator,120)、hover →accent、checked → bgaccent);進度條 (12px、區塊accent)、計數器 「已完成 N / M」、透過QVariantAnimation(280ms) 實現QProgressBar動畫;HoverButton("重設")按鈕。 -
輸入 — 欄位:
- 姓名、電子郵件 — 標準
QLineEdit(34px、圓角 8、hover 邊框rgba(sep,140)、 focus → 邊框accent+ 色調rgba(accent,8)、placeholdertext_secondary、 selectionaccent+accent_fg)。 - 密碼 —
_build_password_field:尾隨操作「眼睛」— 圖示在eye↔eye_slash之間切換、點擊切換EchoMode.Password↔Normal。 - 搜尋 —
_build_search_field:藥丸形圓角 17、前導放大鏡圖示、尾隨 叉叉圖示、僅在文字非空時可見(點擊 — 清除)。 - 多行文字 —
QTextEdit(圓角 10、透過palette.setColor(QPalette.ColorRole.PlaceholderText, ...)設定 placeholder — QSS 選擇器QTextEdit[placeholderText]不可靠) + 右側的字元計數器。 - 欄位圖示在
_field_icon中繪製 (18px:放大鏡 = 圓圈 + f 手柄;叉叉 =rgba(text_secondary,110)的圓圈 + 顏色為theme.bg的「✕」) 或從 SVG 渲染 (_svg_icon): 渲染前,SVG 中的currentColor被替換為theme.text_secondary(請參閱陷阱)。
- 姓名、電子郵件 — 標準
-
聊天 — 請參閱第 8 節。
-
檔案 — 請參閱第 9 節。
-
設定 — 「外觀」卡片(切換開關「深色主題」→
_on_theme_toggled、 「介面大小」區段)、「行為」卡片(切換開關 自動更新 ✓、動畫 ✓)、 下拉選單「介面語言」(QSS:欄位surface+邊框、QAbstractItemView—selection-background-color: accent; selection-color: accent_fg)、滑桿 「動畫速度」=50。
8. 聊天(「聊天」頁面)
- 容器 — 透明
QScrollArea(無框;為避免白色背景:QSSbackground: transparent+viewport().setStyleSheet(...transparent)+ 內部QWidget透明)。 - 訊息儲存在 `self._chat_messages: list[(text, m