为什么你应该关心 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_ButtonMarginPM_LayoutLeftMarginPM_ScrollBarExtent
  • 尺寸类PM_IndicatorWidthPM_SliderLengthPM_TabBarTabHSpace
  • 偏移类PM_MenuBarItemSpacingPM_ToolBarItemSpacing

关键洞察:很多开发者在自定义 style 时忽视 pixelMetric,转而在 sizeFromContents() 里硬编码数字。这是个架构上的错误。pixelMetric单一事实来源——如果你在 sizeFromContentsdrawControl 里分别写了 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;

drawControldrawPrimitive 高一层。一个 control 通常由多个 primitives 组成,并且带有语义信息。比如 CE_PushButton 的绘制包括:

  1. 调用 drawPrimitive(PE_PanelButtonCommand, ...) 画背景
  2. 调用 drawPrimitive(PE_FrameFocusRect, ...) 画焦点框
  3. 调用 drawItemText(...) 画按钮文字
  4. 如果有图标,调用 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 的颜色角色系统支持:

  1. 主题切换:暗色模式 / 亮色模式只需换一个 palette
  2. 无障碍访问:高对比度模式通过 palette 实现
  3. 平台适配: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 变慢的陷阱

避免在绘制函数里分配内存

drawPrimitivedrawControldrawComplexControl 在每次重绘时被调用——可能高达 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 的最佳实践:

架构亮点

  1. 分离主题数据与绘制逻辑:Qlementine 将颜色、间距、圆角等数据抽成独立的 Theme 类,style 只负责"查数据 + 画”。这样换主题只需要换数据对象,style 代码完全不动。

  2. 完整的 animation 支持:Qlementine 在 style 层面实现了过渡动画,而不是依赖 widget 级别的 QPropertyAnimation。这更高效,因为动画状态在绘制时直接计算,避免了不必要的 widget update。

  3. NinePatch 绘制:对于可变尺寸的控件背景,Qlementine 使用 NinePatch(九宫格拉伸)技术,确保边框圆角不会因为拉伸而变形。

  4. 严格的 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 实现一套完整的设计系统中的按钮、输入框、滚动条等常见控件。


参考资料