对照源文件:lib/include/oclero/qlementine/style/QlementineStyle.hpp, lib/include/oclero/qlementine/utils/StateUtils.hpp


1. 继承链:QObject → QStyle → QCommonStyle → QProxyStyle

1.1 四层结构

QObject
 └── QStyle              ← 抽象接口层(pure virtual 方法)
      └── QCommonStyle    ← 默认实现层(所有控件都有基础绘制)
           └── QProxyStyle ← 代理层(包装另一个 Style)
角色 你该用它做什么
QStyle 定义接口。drawPrimitive(), drawControl(), pixelMetric() 全是虚函数。 几乎不会直接继承——工作量巨大。
QCommonStyle Qt 提供的"中等完整度"实现。按钮、菜单、滑块、滚动条……全都有基础画法。 最佳起点:只 override 你要改的部分,其余走基类 fallback。
QProxyStyle 包裹一个已有的 Style,可以拦截/修改部分行为后转发。 适合"在 Fusion 基础上只改按钮颜色"这类局部定制。

1.2 为什么 QCommonStyle 是最佳起点,而非 QProxyStyle

// ❌ QProxyStyle 的典型用法——很快会撞墙
class MyProxyStyle : public QProxyStyle {
public:
    using QProxyStyle::QProxyStyle;
    void drawControl(ControlElement ce, const QStyleOption* opt,
                     QPainter* p, const QWidget* w) const override {
        if (ce == CE_PushButton) {
            // 只能做"额外的绘制",底层还是原来的风格
            // 想改按钮的圆角半径?做不到——pixelMetric 也能 override,
            // 但原始 Style 的 drawControl 根本不会调用你的 pixelMetric
        }
        QProxyStyle::drawControl(ce, opt, p, w);
    }
};

QProxyStyle 的两个致命问题:

  1. 控制倒置:你在别人的绘制结果上"贴补丁”,不是从零构建。
  2. 耦合上游:换一个 base style,你的补丁可能全部失效。

Pitfall:QProxyStyle 的 drawComplexControl 陷阱

QProxyStyledrawComplexControl() 默认转发给 base style。但如果你只 override 了 drawPrimitive 而没 override drawComplexControl,那么 compound control(如 QSpinBox)内部的 primitive 绘制仍然走 base style。你的 change 只对直接调用 drawPrimitive 的控件生效。

QCommonStyle 的优势:

  • 它是"实现”,不是"代理”。你 override 的方法就是最终绘制逻辑。
  • 内部实现已经拆成了大量可重用的 primitive / control 调用,可以直接复用。
  • qlementine 选择了它:class QlementineStyle : public QCommonStyle
// ✅ 正确起点
#include <QCommonStyle>

class MyStyle : public QCommonStyle {
    Q_OBJECT
public:
    using QCommonStyle::QCommonStyle;

    void drawPrimitive(PrimitiveElement pe, const QStyleOption* opt,
                       QPainter* p, const QWidget* w = nullptr) const override;
    void drawControl(ControlElement ce, const QStyleOption* opt,
                     QPainter* p, const QWidget* w = nullptr) const override;
    int pixelMetric(PixelMetric m, const QStyleOption* opt = nullptr,
                    const QWidget* w = nullptr) const override;
};

2. 绘制三剑客:drawPrimitive / drawControl / drawComplexControl

QStyle 将 UI 渲染拆成三层递进的抽象。

2.1 三剑客速览

drawPrimitive()       原子组件:面板、边框、箭头、勾选标记……
         ↑ 被调用
drawControl()         单个控件:按钮、复选框、单选框、标签……
         ↑ 被调用
drawComplexControl()  复合控件:滑块(手柄+凹槽)、滚动条、SpinBox……
方法 粒度 例子 输入类型
drawPrimitive(PrimitiveElement, QStyleOption*, QPainter*) 原子 PE_FrameButtonBevel, PE_IndicatorArrowDown QStyleOption
drawControl(ControlElement, QStyleOption*, QPainter*, QWidget*) 控件 CE_PushButton, CE_CheckBox QStyleOption 子类
drawComplexControl(ComplexControl, QStyleOptionComplex*, QPainter*, QWidget*) 复合 CC_SpinBox, CC_Slider QStyleOptionComplex

2.2 拆解一个 SpinBox:调用链全景

QSpinBox::paintEvent()
  └─→ style()->drawComplexControl(CC_SpinBox, opt, painter, this);
        │
        ├─→ drawPrimitive(PE_PanelButtonBevel, ...)     // 整体边框面板
        ├─→ drawPrimitive(PE_FrameLineEdit, ...)        // 编辑框背景
        ├─→ drawControl(CE_SpinBoxPlus, ...)            // ▲ 上按钮
        │     └─→ drawPrimitive(PE_IndicatorSpinUp, ...)
        └─→ drawControl(CE_SpinBoxMinus, ...)           // ▼ 下按钮
              └─→ drawPrimitive(PE_IndicatorSpinDown, ...)

关键洞察:

  • drawComplexControl 负责布局(subControlRect 决定每个部分的位置)
  • drawControl 负责单个子控件的绘制
  • drawPrimitive 负责最底层的形状/图案

2.3 还有两个辅助角色

方法 作用 典型用法
subControlRect(ComplexControl, QStyleOptionComplex*, SubControl, QWidget*) 返回子控件的矩形区域 “▲ 按钮在这个位置,大小 16×16”
sizeFromContents(ContentsType, QStyleOption*, QSize, QWidget*) 确定控件的总大小 “按钮内容大小 + padding = 按钮大小”

3. QStyleOption 系统

3.1 关键子类

QStyleOption
 ├── QStyleOptionButton        // 按钮:text, icon, features
 ├── QStyleOptionComplex       // 复合控件基类:subControls, activeSubControls
 │    ├── QStyleOptionSlider
 │    └── QStyleOptionSpinBox
 ├── QStyleOptionMenuItem
 ├── QStyleOptionTab
 ├── QStyleOptionViewItem
 └── QStyleOptionFrame

所有子类共享关键字段:QStyle::State state(QFlags)。

3.2 qstyleoption_cast:安全向下转型

void MyStyle::drawControl(ControlElement ce, const QStyleOption* opt,
                          QPainter* p, const QWidget* w) const {
    if (ce == CE_PushButton) {
        // ✅ 安全:类型不匹配时返回 nullptr
        const auto* btnOpt = qstyleoption_cast<const QStyleOptionButton*>(opt);
        if (!btnOpt) {
            QCommonStyle::drawControl(ce, opt, p, w);
            return;
        }
        p->drawText(btnOpt->rect, Qt::AlignCenter, btnOpt->text);
    } else {
        QCommonStyle::drawControl(ce, opt, p, w);
    }
}

不要用 static_castdynamic_castQStyleOption 没有虚析构,用 qstyleoption_cast 通过检查 opt->type 字段做安全转换。

3.3 State 标志位

QStyle::State_Enabled      // 控件可用
QStyle::State_MouseOver    // 鼠标悬停
QStyle::State_Sunken       // 被按下/下沉状态
QStyle::State_HasFocus     // 键盘焦点
QStyle::State_Selected     // 被选中
QStyle::State_On           // 切换开(勾选/选中)
QStyle::State_Active       // 所属窗口是活动窗口

4. State 抽象:从位标志到语义化枚举

4.1 问题

直接在绘制代码里 testFlag 做分支:到处重复、优先级隐式、不利于主题查表。

4.2 解决方案:引入 MouseState / FocusState / CheckState

enum class MouseState {
    Normal, Hovered, Pressed, Disabled,
};

enum class FocusState { NotFocused, Focused };

enum class CheckState { NotChecked, Checked, Indeterminate };

4.3 映射函数

MouseState getMouseState(QStyle::State const& state) {
    // 注意:Disabled 优先——灰掉的不应该有 hover/press 表现
    if (!state.testFlag(QStyle::State_Enabled)) return MouseState::Disabled;
    if (state.testFlag(QStyle::State_Sunken))   return MouseState::Pressed;
    if (state.testFlag(QStyle::State_MouseOver)) return MouseState::Hovered;
    return MouseState::Normal;
}

优先级顺序至关重要——Disabled 必须排第一。


5. 动手:最小可运行的 QCommonStyle 子类

// simplestyle.hpp
class SimpleStyle : public QCommonStyle {
    Q_OBJECT
public:
    using QCommonStyle::QCommonStyle;
    void drawPrimitive(PrimitiveElement pe, const QStyleOption* opt,
                       QPainter* p, const QWidget* w = nullptr) const override;
    void drawControl(ControlElement ce, const QStyleOption* opt,
                     QPainter* p, const QWidget* w = nullptr) const override;
};
// simplestyle.cpp - 用颜色常量画按钮
void SimpleStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption* opt,
                                QPainter* p, const QWidget* w) const {
    switch (pe) {
    case PE_FrameButtonBevel: {
        const auto mouse = getMouseState(opt->state);
        const QColor btnBg[] = {
            QColor("#4096ff"),  // Normal
            QColor("#73b3ff"),  // Hovered
            QColor("#1a6cdc"),  // Pressed
            QColor("#cccccc"),  // Disabled
        };
        p->save();
        p->setRenderHint(QPainter::Antialiasing);
        p->setPen(Qt::NoPen);
        p->setBrush(btnBg[static_cast<int>(mouse)]);
        p->drawRoundedRect(opt->rect, 6, 6);
        p->restore();
        break;
    }
    default:
        QCommonStyle::drawPrimitive(pe, opt, p, w);
        break;
    }
}
// main.cpp
int main(int argc, char* argv[]) {
    QApplication app(argc, argv);
    app.setStyle(new SimpleStyle);  // 必须在创建任何 widget 之前!
    QPushButton btn("Hello QStyle!");
    btn.show();
    return app.exec();
}

6. Pitfalls 合集

  1. drawPrimitive 只依赖 QStyleOption 基类字段(state, rect, direction),不要假设类型
  2. 改变了外观要同步重写 pixelMetric,否则布局错位
  3. 不处理的 case 必须调用基类,否则控件消失
  4. State 顺序:Disabled > Pressed > Hovered > Normal

7. 对照 qlementine 源文件

概念 qlementine 文件
QStyle 继承 QlementineStyle.hppclass QlementineStyle : public QCommonStyle
State 枚举 Common.hppenum class MouseState, FocusState, CheckState
State 映射 StateUtils.cppgetMouseState(), getFocusState()

下一课:Theme 系统设计 →