对照源文件:
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 的两个致命问题:
- 控制倒置:你在别人的绘制结果上"贴补丁”,不是从零构建。
- 耦合上游:换一个 base style,你的补丁可能全部失效。
Pitfall:QProxyStyle 的
drawComplexControl陷阱
QProxyStyle的drawComplexControl()默认转发给 base style。但如果你只 override 了drawPrimitive而没 overridedrawComplexControl,那么 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_cast或dynamic_cast—QStyleOption没有虚析构,用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 合集
drawPrimitive只依赖QStyleOption基类字段(state, rect, direction),不要假设类型- 改变了外观要同步重写
pixelMetric,否则布局错位 - 不处理的 case 必须调用基类,否则控件消失
- State 顺序:Disabled > Pressed > Hovered > Normal
7. 对照 qlementine 源文件
| 概念 | qlementine 文件 |
|---|---|
| QStyle 继承 | QlementineStyle.hpp — class QlementineStyle : public QCommonStyle |
| State 枚举 | Common.hpp — enum class MouseState, FocusState, CheckState |
| State 映射 | StateUtils.cpp — getMouseState(), getFocusState() |
下一课:Theme 系统设计 →