为什么你应该关心 QStyle 的内部机制
大多数 Qt 开发者对 QStyle 的认知停留在 QApplication::setStyle("Fusion") 或者换个主题色。但如果你正在做以下任何一件事,理解 QStyle 内核就不是"加分项"而是"必修课”:
- 做一个真正有辨识度的桌面应用,而不是"又一个 Fusion 换皮”
- 需要从零实现一套设计系统的完整映射
- 想搞懂 Qt 的绘制管线,写出高性能的自定义控件
- 给别人写 UI 组件库,让他们用你的 style 就能获得一致的视觉语言
这篇文章将带你穿过 QStyle 的表层 API,进入它的内核——那些 Qt 官方文档一笔带过、但实际决定了一个 style 品质的细节。
QStyle 的架构全景
在深入细节之前,先建立一张 mental map:
graph TB
subgraph 应用层
W[QWidget / QPainter]
end
subgraph 抽象基类
QS["<b>QStyle</b><br/>(抽象基类)"]
QS --> Q[查询接口]
QS --> D[绘制接口]
Q --> Q1[pixelMetric]
Q --> Q2[styleHint]
Q --> Q3[subElementRect]
Q --> Q4[sizeFromContents]
D --> D1[drawPrimitive]
D --> D2[drawControl]
D --> D3[drawComplexControl]
end
subgraph 继承链
QCS[QCommonStyle] --> QPS[QProxyStyle] --> YS["你的 Style"]
end
W -->|调用| QS
QS -->|继承| QCS
style QS fill:#4A90D9,color:#fff,stroke:#2C5F8A
style YS fill:#27AE60,color:#fff,stroke:#1E8449
style W fill:#95A5A6,color:#fff,stroke:#7F8C8D
style QPS fill:#E67E22,color:#fff,stroke:#D35400
style QCS fill:#E67E22,color:#fff,stroke:#D35400
QStyle 本质上是一个策略模式的巨型实例。Qt 把"控件应该长什么样"的所有决策集中到一个对象里,让控件的逻辑和外观彻底解耦。这不是什么新鲜想法,但 Qt 把它做到了极致——整个绘制管线、几何计算、交互反馈全部通过这一个接口流转。
第一层:查询接口——Style 的"配置系统”
在画任何东西之前,style 先要回答一系列"是什么"的问题。这些查询接口构成了 style 的配置层。
pixelMetric():一切尺寸的源头
int pixelMetric(PixelMetric metric,
const QStyleOption *option = nullptr,
const QWidget *widget = nullptr) const;
这是 QStyle 中被调用频率最高的函数之一。Qt 定义了超过 80 个 PixelMetric 枚举值,覆盖了:
- 间距类:
PM_ButtonMargin、PM_LayoutLeftMargin、PM_ScrollBarExtent - 尺寸类:
PM_IndicatorWidth、PM_SliderLength、PM_TabBarTabHSpace - 偏移类:
PM_MenuBarItemSpacing、PM_ToolBarItemSpacing
关键洞察:很多开发者在自定义 style 时忽视 pixelMetric,转而在 sizeFromContents() 里硬编码数字。这是个架构上的错误。pixelMetric 是单一事实来源——如果你在 sizeFromContents 和 drawControl 里分别写了 24,将来想改成 28 就得改两处,而且很可能漏掉第三处。
正确做法:定义一组内部的 metric 常量,所有计算都引用它们:
// 好的实践:集中定义,全局引用
class MyStyle : public QProxyStyle {
enum InternalMetrics {
ButtonHMargin = 16,
ButtonVMargin = 6,
SliderThickness = 4,
// ...
};
int pixelMetric(PixelMetric metric,
const QStyleOption *opt,
const QWidget *w) const override {
switch (metric) {
case PM_ButtonMargin:
return ButtonHMargin;
case PM_ButtonDefaultIndicator:
return 0; // 不使用默认按钮的额外指示器
// ...
}
return QProxyStyle::pixelMetric(metric, opt, w);
}
};
styleHint():行为开关
int styleHint(StyleHint hint,
const QStyleOption *option = nullptr,
const QWidget *widget = nullptr,
QStyleHintReturn *returnData = nullptr) const;
如果说 pixelMetric 管"多大”,styleHint 就管"怎么做”。它定义了超过 100 个行为标志:
| Hint | 含义 | 影响 |
|---|---|---|
SH_EtchDisabledText |
禁用文字是否加浮雕效果 | 所有禁用文本的绘制 |
SH_ScrollBar_ContextMenu |
滚动条是否支持右键菜单 | 滚动条交互行为 |
SH_TabBar_Alignment |
标签栏对齐方式 | 整个 TabBar 布局 |
SH_UnderlineShortcut |
是否显示快捷键下划线 | 菜单和按钮文字渲染 |
SH_DialogButtonBox_ButtonsHaveIcons |
对话框按钮是否带图标 | QDialogButtonBox 外观 |
容易被忽视的细节:styleHint 的第四个参数 returnData 是一个 QStyleHintReturn 指针。某些 hint 不仅需要返回一个数值,还通过这个指针输出结构体数据。比如 SH_RubberBand_Mask 提示返回一个 QStyleHintReturnMask,包含橡皮筋选区的精确区域。如果你的 style 不支持这些需要返回复杂数据的 hint,某些 Qt 内部行为就会退化到默认实现,可能导致微妙的外观不一致。
subElementRect() 与 subControlRect():几何的"分区治理”
QRect subElementRect(SubElement element,
const QStyleOption *option,
const QWidget *widget) const;
QRect subControlRect(ComplexControl control,
const QStyleOptionComplex *option,
SubControl subControl,
const QWidget *widget) const;
这两个函数是 QStyle 中最精妙的设计之一。它们解决了一个核心问题:一个复杂控件的不同部分,由谁来决定它们的几何位置?
SubElement针对简单控件的子部件:比如SE_PushButtonFocusRect(按钮的焦点矩形相对按钮的位置)SubControl针对复杂控件的子控件:比如SC_ScrollBarSlider(滚动条滑块相对滚动条的位置)
设计意图:控件本身不计算子元素的几何位置,而是向 style 发起查询。这样换一个 style,同一个复杂控件的内部布局可以完全不同。一个极端的例子:macOS 风格的 QSpinBox 上下按钮是并排的,而 Windows 风格是叠放的——这个差异完全由 subControlRect() 的返回值驱动,QSpinBox 自身的代码一行不改。
// QSpinBox 内部大概是这样获取按钮位置的(伪代码):
QRect upButtonRect = style()->subControlRect(
CC_SpinBox, &opt, SC_SpinBoxUp, this);
QRect downButtonRect = style()->subControlRect(
CC_SpinBox, &opt, SC_SpinBoxDown, this);
在实践中踩过的坑:当你自定义 style 时,如果在 subControlRect() 中返回的矩形与随后在 drawComplexControl() 中实际绘制的区域不一致,就会出现"点击热区"和"视觉区域"分离的 bug——看起来像点中了但实际上事件被吞掉了,或者反过来。
sizeFromContents():从内容到尺寸的完整链条
QSize sizeFromContents(ContentsType type,
const QStyleOption *option,
const QSize &contentsSize,
const QWidget *widget) const;
这个函数接受"内容需要多大”(contentsSize),返回"控件应该多大”。它是 QWidget::sizeHint() 调用的终点。
调用链:
sequenceDiagram
participant W as QWidget
participant S as QStyle
W->>S: sizeHint()
S->>S: 计算 contentSize<br/>(文字 + 图标 + 间距)
S->>S: sizeFromContents(CT_XXX, opt, contentSize, widget)
Note right of S: ① 获取内容 natural size<br/>② 加上 padding / margin / border<br/>③ 应用 min/max 限制
S-->>W: 返回最终 QSize
值得注意的细节:contentsSize 参数是由 Qt 计算好的——对于按钮来说,它已经包含了文字宽度、图标尺寸、两者间距。你的工作只是加上外包装。这意味着如果你重写 sizeFromContents 时没有正确地给内容加边距,会导致文字贴在控件边缘或者溢出。
第二层:绘制接口——Style 的"渲染管线”
drawPrimitive():最基本的视觉原子
void drawPrimitive(PrimitiveElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget = nullptr) const;
drawPrimitive 负责绘制不可再分的视觉基元。Qt 定义了大约 30 种 primitives:
PE_Frame:框架线PE_PanelButtonCommand:按钮面板PE_IndicatorCheckBox:复选框勾选标记PE_IndicatorArrowUp/Down/Left/Right:箭头指示器
关键认知:Primitives 是"无状态的”。PE_PanelButtonCommand 收到的 QStyleOptionButton 里包含了按钮的所有状态(按下、悬停、禁用、默认),但 primitive 本身不管理状态——它只是"给你这些参数,你画出来”。
这意味着你的 drawPrimitive 实现中的状态判断会非常密集:
void MyStyle::drawPrimitive(PrimitiveElement elem,
const QStyleOption *opt,
QPainter *p,
const QWidget *w) const {
switch (elem) {
case PE_PanelButtonCommand: {
const auto *btn = qstyleoption_cast<const QStyleOptionButton*>(opt);
if (!btn) break;
bool isDown = btn->state & State_Sunken;
bool isHover = btn->state & State_MouseOver;
bool isDefault = btn->features & QStyleOptionButton::DefaultButton;
bool isDisabled = !(btn->state & State_Enabled);
// 根据状态组合选择颜色和效果
QColor bg = isDisabled ? disabledBg
: isDown ? pressedBg
: isHover ? hoveredBg
: isDefault ? accentBg
: normalBg;
drawRoundedPanel(p, btn->rect, bg, borderRadius);
break;
}
default:
QProxyStyle::drawPrimitive(elem, opt, p, w);
}
}
drawControl():带语义的控件绘制
void drawControl(ControlElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget = nullptr) const;
drawControl 比 drawPrimitive 高一层。一个 control 通常由多个 primitives 组成,并且带有语义信息。比如 CE_PushButton 的绘制包括:
- 调用
drawPrimitive(PE_PanelButtonCommand, ...)画背景 - 调用
drawPrimitive(PE_FrameFocusRect, ...)画焦点框 - 调用
drawItemText(...)画按钮文字 - 如果有图标,调用
drawItemPixmap(...)画图标
这里有一个常见的陷阱:在自定义 drawControl 时直接画所有东西,跳过内部的 drawPrimitive 调用。这会导致你的 style 中某些 primitives 被重写了但按钮不生效——因为你绕过了它们。正确做法是:
void MyStyle::drawControl(ControlElement elem,
const QStyleOption *opt,
QPainter *p,
const QWidget *w) const {
switch (elem) {
case CE_PushButton: {
// ✅ 正确:通过 drawPrimitive 绘制背景
drawPrimitive(PE_PanelButtonCommand, opt, p, w);
// 然后只处理文字和图标布局
// ...
break;
}
}
}
drawComplexControl():交互式复合控件
void drawComplexControl(ComplexControl control,
const QStyleOptionComplex *option,
QPainter *painter,
const QWidget *widget = nullptr) const;
这是绘制接口中最复杂的一层。QStyleOptionComplex 不同于普通的 QStyleOption,它额外提供了 subControls 字段——一个 SubControls 位掩码,标志着当前活跃的子控件。
典型的绘制流程(以 CC_ScrollBar 为例):
graph TD
DCC["<b>drawComplexControl</b><br/>CC_ScrollBar"]
DCC --> S1["drawPrimitive<br/>PE_PanelScrollArea<br/><i>画轨道背景</i>"]
DCC --> S2["<b>subControlRect</b><br/>查询:滑块几何位置"]
DCC --> S3["drawPrimitive<br/>PE_IndicatorScrollBarSlider<br/><i>画滑块</i>"]
DCC --> S4["<b>subControlRect</b><br/>查询:上按钮几何位置"]
DCC --> S5["drawPrimitive<br/>PE_IndicatorArrowUp<br/><i>画上箭头</i>"]
DCC --> S6["<b>subControlRect</b><br/>查询:下按钮几何位置"]
DCC --> S7["drawPrimitive<br/>PE_IndicatorArrowDown<br/><i>画下箭头</i>"]
style DCC fill:#8E44AD,color:#fff,stroke:#6C3483
style S2 fill:#3498DB,color:#fff,stroke:#2471A3
style S4 fill:#3498DB,color:#fff,stroke:#2471A3
style S6 fill:#3498DB,color:#fff,stroke:#2471A3
style S1 fill:#27AE60,color:#fff,stroke:#1E8449
style S3 fill:#27AE60,color:#fff,stroke:#1E8449
style S5 fill:#27AE60,color:#fff,stroke:#1E8449
style S7 fill:#27AE60,color:#fff,stroke:#1E8449
最重要的细节:option->activeSubControls 让你知道用户正在与哪个子控件交互(比如滑块正在被拖动),从而可以绘制不同的视觉状态。这个字段在 drawComplexControl 被调用前由 Qt 通过 hitTestComplexControl() 确定。
绘制管线的完整数据流
graph TD
PE["<b>QWidget::paintEvent()</b>"]
PE -->|简单控件<br/>QPushButton / QLabel| ISO["<b>initStyleOption()</b><br/>填充 QStyleOption"]
PE -->|复杂控件<br/>QScrollBar / QSpinBox| ICO["<b>initStyleOption()</b><br/>填充 QStyleOptionComplex"]
ISO --> DC["style()->drawControl<br/>CE_PushButton"]
DC --> DP1["drawPrimitive<br/>PE_PanelButtonCommand<br/><i>背景面板</i>"]
DC --> DP2["drawPrimitive<br/>PE_FrameFocusRect<br/><i>焦点框</i>"]
DC --> DI["drawItemText<br/><i>文字 + 图标</i>"]
ICO --> DCC["style()->drawComplexControl<br/>CC_ScrollBar"]
DCC --> SCR["<b>subControlRect</b><br/>× N 次<br/><i>确定各子控件位置</i>"]
DCC --> DPR["<b>drawPrimitive</b><br/>× N 次<br/><i>绘制各子控件</i>"]
DCC --> DCT["<b>drawControl</b><br/>× N 次<br/><i>内嵌简单控件</i>"]
style PE fill:#C0392B,color:#fff,stroke:#922B21
style ISO fill:#E67E22,color:#fff,stroke:#CA6F1E
style ICO fill:#E67E22,color:#fff,stroke:#CA6F1E
style DC fill:#8E44AD,color:#fff,stroke:#6C3483
style DCC fill:#8E44AD,color:#fff,stroke:#6C3483
style DP1 fill:#27AE60,color:#fff,stroke:#1E8449
style DP2 fill:#27AE60,color:#fff,stroke:#1E8449
style DI fill:#3498DB,color:#fff,stroke:#2471A3
style SCR fill:#3498DB,color:#fff,stroke:#2471A3
style DPR fill:#27AE60,color:#fff,stroke:#1E8449
style DCT fill:#2980B9,color:#fff,stroke:#1F6F8B
第三层:生命周期——polish 与 unpolish
void polish(QWidget *widget);
void polish(QApplication *app);
void polish(QPalette &palette);
void unpolish(QWidget *widget);
void unpolish(QApplication *app);
polish/unpolish 是 QStyle 中最容易被误解的部分。它们不是"美化"的意思,而是”初始化和清理"。
polish(QApplication*) 的调用时机
这是最早被调用的 polish。只调用一次。在这里你可以:
- 修改全局调色板
QApplication::setPalette() - 注册全局事件过滤器
- 加载字体
polish(QWidget*) 的调用时机
每次一个 widget 被创建、或者 style 被切换时,每个 widget 都会被 polish。这里是设置 widget 级别属性的地方,但注意限制:
void MyStyle::polish(QWidget *w) {
// ✅ 安全操作:
w->setAttribute(Qt::WA_Hover, true); // 启用悬停检测
if (auto *btn = qobject_cast<QPushButton*>(w)) {
btn->setAutoFillBackground(false); // 禁用自动背景填充
}
// ❌ 危险操作:不要在 polish 里设置样式表
// w->setStyleSheet("background: red;"); // 会破坏样式继承
QProxyStyle::polish(w);
}
一个容易被忽视的规则:polish(widget) 中调用 widget->setAttribute() 是安全的;但调用 widget->setFont() 需要小心——它会覆盖应用程序级别的字体设置,导致某些平台出现文字大小不一致的问题。
unpolish 的重要性
很多自定义 style 的实现者完全不写 unpolish,这在运行时切换 style 时会导致问题。unpolish 是在 style 被替换之前调用的,你需要在这里撤销 polish 中设置的内容。
void MyStyle::unpolish(QWidget *w) {
// 撤销 polish 中设置的内容
w->setAttribute(Qt::WA_Hover, false);
QProxyStyle::unpolish(w);
}
如果你不写 unpolish,切换到另一个 style 时 widget 会带着前一个 style 设置的属性,导致新的 style 行为异常。
第四层:QProxyStyle——继承而不是重写一切
QProxyStyle 是自定义 style 的最佳起点。它本质上是一个装饰器模式:包装一个基础 style(默认是系统原生 style),只覆盖你关心的方法,其余全部透传。
class MyStyle : public QProxyStyle {
public:
using QProxyStyle::QProxyStyle; // 继承构造函数
// 显式指定基础 style
MyStyle() : QProxyStyle("Fusion") {}
// 只覆盖需要改的方法
void drawPrimitive(...) override;
int pixelMetric(...) override;
// 其他 20+ 个虚函数全部透传给 Fusion
};
选择基础 style 的策略:
| 基础 Style | 适合场景 |
|---|---|
Fusion |
跨平台一致的桌面应用,几乎总是最好的起点 |
Windows / windowsvista |
仅在 Windows 上使用,需要原生外观 |
macOS |
仅在 macOS 上使用 |
| 默认(不指定) | 依赖平台原生 style,行为不确定,不推荐 |
为什么选 Fusion 作为基础:Fusion 是用纯 QPainter 绘制的,不依赖平台原生 API。这意味着你的自定义 style 也天然跨平台。如果你基于 Windows style 做自定义,到 macOS 上可能完全画不出来。
第五层:QPalette——色彩系统的正确打开方式
很多开发者在自定义 style 时直接硬编码颜色:
// ❌ 坏实践
p->fillRect(rect, QColor("#4A90D9"));
更好的方式是使用 QPalette:
// ✅ 好实践
QColor color = opt->palette.color(QPalette::Button);
p->fillRect(rect, color);
为什么 palette 重要:Qt 的颜色角色系统支持:
- 主题切换:暗色模式 / 亮色模式只需换一个 palette
- 无障碍访问:高对比度模式通过 palette 实现
- 平台适配:macOS 和 Windows 的系统强调色自动映射到对应角色
定义一个完整的主题 palette
QPalette createDarkPalette() {
QPalette pal;
// 基础色
pal.setColor(QPalette::Window, QColor(30, 30, 30));
pal.setColor(QPalette::WindowText, Qt::white);
pal.setColor(QPalette::Base, QColor(42, 42, 42));
pal.setColor(QPalette::AlternateBase, QColor(50, 50, 50));
pal.setColor(QPalette::ToolTipBase, QColor(50, 50, 50));
pal.setColor(QPalette::ToolTipText, Qt::white);
pal.setColor(QPalette::Text, Qt::white);
pal.setColor(QPalette::Button, QColor(49, 49, 49));
pal.setColor(QPalette::ButtonText, Qt::white);
pal.setColor(QPalette::BrightText, Qt::red);
pal.setColor(QPalette::Link, QColor(42, 130, 218));
pal.setColor(QPalette::Highlight, QColor(42, 130, 218));
pal.setColor(QPalette::HighlightedText, Qt::white);
// 禁用态颜色组
pal.setColor(QPalette::Disabled, QPalette::WindowText,
QColor(127, 127, 127));
pal.setColor(QPalette::Disabled, QPalette::Text,
QColor(127, 127, 127));
pal.setColor(QPalette::Disabled, QPalette::ButtonText,
QColor(127, 127, 127));
return pal;
}
三个颜色组
每个 QPalette 包含三个颜色组:
Active:窗口处于激活状态Inactive:窗口处于非激活状态(后台)Disabled:控件被禁用
很多 style 实现者的遗漏:他们只定义了 Active 组的颜色,导致禁用状态下的控件和激活状态下看起来几乎一样,用户无法区分。
第六层:事件系统——QStyle 如何感知交互
QStyle 本身不直接处理事件,但 style 的实现质量取决于你对 Qt 事件系统的理解。
hover 效果的前提
Qt 默认不启用 hover 检测(出于性能考虑)。如果你的 style 需要 hover 效果(几乎所有现代 UI 都需要),必须在 polish 中设置:
void MyStyle::polish(QWidget *w) {
w->setAttribute(Qt::WA_Hover, true);
QProxyStyle::polish(w);
}
设置之后,QStyleOption::state 中才会出现 State_MouseOver 标志。
QStyleOption 的状态字段
QStyleOption::state 是一个位掩码,包含以下关键标志:
| 标志 | 含义 |
|---|---|
State_Enabled |
控件可用 |
State_MouseOver |
鼠标悬停(需要 WA_Hover) |
State_Sunken |
按下状态 |
State_HasFocus |
拥有键盘焦点 |
State_Selected |
被选中 |
State_On |
开关状态为"开” |
State_ReadOnly |
只读模式 |
把这些状态正确映射为视觉表现是一个优秀 style 的核心能力。
第七层:性能考量——那些让你 Style 变慢的陷阱
避免在绘制函数里分配内存
drawPrimitive、drawControl、drawComplexControl 在每次重绘时被调用——可能高达 60fps。在其中分配堆内存是不可接受的:
// ❌ 每次重绘都分配内存
void MyStyle::drawPrimitive(...) {
QPixmap cache(size());
QPainter cachePainter(&cache);
// pixmap 的创建和销毁是昂贵的
}
// ✅ 缓存或复用资源
void MyStyle::drawPrimitive(...) {
// 使用预渲染的 NinePatch 或缓存
p->drawPixmap(rect, themeCache.buttonBackground(rect.size()));
}
合理使用 QPixmapCache
Qt 提供了 QPixmapCache 全局缓存,适合缓存经常重复绘制的小图形:
QPixmap MyStyle::cachedPixmap(const QString &key,
const QSize &size) {
QPixmap pm;
if (!QPixmapCache::find(key, &pm)) {
pm = renderExpensiveGraphic(size);
QPixmapCache::insert(key, pm);
}
return pm;
}
hitTestComplexControl 的性能
hitTestComplexControl 在鼠标移动时高频调用。不要在 hitTest 里做复杂计算——它应该就是一系列矩形判断:
SubControl MyStyle::hitTestComplexControl(
ComplexControl control,
const QStyleOptionComplex *opt,
const QPoint &pos,
const QWidget *w) const {
// ✅ 直接判断,不要重算几何
if (subControlRect(control, opt, SC_SliderHandle, w).contains(pos))
return SC_SliderHandle;
if (subControlRect(control, opt, SC_ScrollBarAddLine, w).contains(pos))
return SC_ScrollBarAddLine;
// ...
return SC_None;
}
第八层:从参考实现中学习——以 Qlementine 为例
Qlementine 是 Olivier Cléro 开发的一个完整的 QStyle 参考实现。它展示了许多工业级 style 的最佳实践:
架构亮点
-
分离主题数据与绘制逻辑:Qlementine 将颜色、间距、圆角等数据抽成独立的
Theme类,style 只负责"查数据 + 画”。这样换主题只需要换数据对象,style 代码完全不动。 -
完整的 animation 支持:Qlementine 在 style 层面实现了过渡动画,而不是依赖 widget 级别的
QPropertyAnimation。这更高效,因为动画状态在绘制时直接计算,避免了不必要的 widget update。 -
NinePatch 绘制:对于可变尺寸的控件背景,Qlementine 使用 NinePatch(九宫格拉伸)技术,确保边框圆角不会因为拉伸而变形。
-
严格的 palette 遵守:Qlementine 几乎所有颜色都从
QPalette读取,用户只需要设置 palette 就能改变整个应用的颜色方案。
关键实现模式
Qlementine 的 drawPrimitive 实现展示了一个清晰的模式:
1. 从 QStyleOption 提取状态信息
2. 根据状态从 Theme 查询对应的视觉属性
3. 使用 QPainter 绘制
4. (可选)应用动画插值
这种分离确保了:
- 状态判断逻辑清晰、可测试
- 视觉数据可以独立调整(换主题)
- 绘制代码可以复用(同样的圆角矩形,不同的颜色)
总结:构建一个优秀 QStyle 的检查清单
| # | 检查项 | 细节 |
|---|---|---|
| 1 | 选对基础 Style | 默认用 Fusion,保持跨平台一致 |
| 2 | polish 里设 WA_Hover | 否则悬停效果全无效 |
| 3 | unpolish 里撤销设置 | 否则 style 切换出 bug |
| 4 | pixelMetric 集中管理所有尺寸 | 不要在 sizeFromContents 里硬编码 |
| 5 | drawControl 通过 drawPrimitive 绘制子元素 | 不要绕过内部调用链 |
| 6 | subControlRect 和绘制保持一致 | 否则点击热区和视觉区域分离 |
| 7 | 颜色从 QPalette 读取 | 不要硬编码,支持暗色/高对比度模式 |
| 8 | 定义 Active/Inactive/Disabled 三组颜色 | 禁用态必须肉眼可区分 |
| 9 | 绘制函数内不分配堆内存 | 用 QPixmapCache 预缓存 |
| 10 | hitTest 只做矩形判断 | 不重算几何、不创建临时对象 |
这篇文章是 Logic’s Lab「Qt 深度定制 UI 框架」系列的第一篇。下一篇我们将深入 QStyle 控件的自定义绘制,包括如何用 QStyle 实现一套完整的设计系统中的按钮、输入框、滚动条等常见控件。
参考资料
- Qt 官方文档:QStyle Class
- Qlementine 项目:github.com/oclero/qlementine
- Qt Style Sheets 与 QStyle 的关系:The Style Sheet Syntax