前三课我们搭好了骨架:QStyle 继承链、Theme 数据层、polish/EventFilter 生命周期。这一课进入整个体系的心脏——绘制引擎。
Qt Style 把"控件怎么画"拆成三层,每层有不同的粒度和职责。理解这三层,你就理解了 QStyle 的全部绘制逻辑。
drawComplexControl ← 复杂控件(ScrollBar, ComboBox, SpinBox)
└─ drawControl ← 独立子区域(PushButton, Tab, MenuBar)
└─ drawPrimitive ← 最小绘制单元(Frame, Bevel, Arrow, Indicator)
1. drawPrimitive 深度解析:PE_FrameButtonBevel
drawPrimitive 是最底层的绘制函数。它只负责画一个"无状态的图形元素”——一个圆角矩形框、一个箭头、一个复选框勾号。它不关心这个元素属于什么控件,只关心颜色和形状。
1.1 签名与调用约定
void QStyle::drawPrimitive(
PrimitiveElement pe,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget = nullptr
) const;
参数含义:
| 参数 | 说明 |
|---|---|
pe |
枚举值,决定画什么(PE_FrameButtonBevel 画按钮底板) |
option |
QStyleOption 子类,携带 state 和几何信息。这里我们收到的是 QStyleOptionButton 的上转型 |
painter |
已初始化的 QPainter,坐标原点已平移到 widget (0,0) |
widget |
可选,原始 widget 指针。大多数时候不需要,用 option->rect 就够了 |
关键认知:Qt 调用 drawPrimitive 之前已经帮你做好了 painter->save() / painter->translate(),退出后也会自动 restore()。你不需要手动处理坐标变换。
1.2 PE_FrameButtonBevel 五步绘制法
我们以画一个按钮的底板作为完整的解剖案例。这个函数需要处理 hover、pressed、checked、disabled、focused 五种视觉状态。
Step 1: 从 QStyleOption 提取状态和颜色角色
void MiniStyle::drawPrimitive(
PrimitiveElement pe,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget
) const {
switch (pe) {
case PE_FrameButtonBevel: {
// --- Step 1: 提取 state 信息 ---
const auto *opt = qstyleoption_cast<const QStyleOptionButton *>(option);
if (!opt)
return;
const bool isHovered = opt->state & State_MouseOver;
const bool isPressed = opt->state & State_Sunken;
const bool isChecked = opt->state & State_On;
const bool isDisabled = !(opt->state & State_Enabled);
const bool isDefault = opt->features & QStyleOptionButton::DefaultButton;
const bool hasFocus = opt->state & State_HasFocus;
// --- Step 2: 从 Theme 查找颜色 ---
MiniTheme theme = MiniThemeManager::currentTheme();
QColor bg = isDisabled ? theme.buttonDisabledBg
: isPressed ? theme.buttonPressedBg
: isHovered ? theme.buttonHoverBg
: isChecked ? theme.buttonCheckedBg
: theme.buttonBg;
QColor border = isDisabled ? theme.borderDisabledColor
: hasFocus ? theme.focusColor
: isDefault ? theme.accentColor
: theme.borderColor;
const QRect rect = opt->rect;
const qreal radius = theme.borderRadius; // e.g. 6.0
const qreal penW = theme.borderWidth; // e.g. 1.5
qstyleoption_cast<T>() 是 Qt 提供的安全下行转型。它在 debug 模式下做类型检查,release 模式等价于 static_cast。永远用它,不要直接用 static_cast。
State_MouseOver、State_Sunken 这些是 QStyle 的核心状态标记。它们是位掩码,所以用 & 检测。Qt 在发送 paintEvent 之前,Framework 层已经根据鼠标事件设置好了这些标记——你只需要读取。
Step 2: Theme 查找 — 颜色映射
// Theme 数据类的简化定义(完整版见第2课)
struct MiniTheme {
// 按钮颜色
QColor buttonBg { "#F0F0F0" };
QColor buttonHoverBg { "#E0E0E0" };
QColor buttonPressedBg { "#CCCCCC" };
QColor buttonCheckedBg { "#C8DDF8" };
QColor buttonDisabledBg { "#F5F5F5" };
// 边框
QColor borderColor { "#C0C0C0" };
QColor borderDisabledColor { "#E0E0E0" };
QColor focusColor { "#4A90D9" };
QColor accentColor { "#2E7BD6" };
// 几何
qreal borderRadius = 6.0;
qreal borderWidth = 1.5;
qreal focusRingWidth = 2.0;
int focusRingMargin = 2; // focus ring 比按钮大 2px
};
设计原则:Theme 是纯数据,不包含任何绘制逻辑。绘制逻辑留在 Style 或自由函数中。这样换肤只需要改 JSON,不需要改 C++。
Step 3: 填充圆角矩形
// --- Step 3: 填充背景 ---
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(Qt::NoPen);
painter->setBrush(bg);
painter->drawRoundedRect(rect, radius, radius);
drawRoundedRect(QRectF, qreal rx, qreal ry) 会自动处理矩形的四个圆角。注意用 QRectF 而非 QRect 可以避免整数截断导致的 1px 偏移。
setRenderHint(Antialiasing) 必须开,否则圆角会有锯齿。这行应该放在函数开头,全局生效。
Step 4: 绘制边框
// --- Step 4: 绘制边框 ---
QPen borderPen(border, penW);
borderPen.setJoinStyle(Qt::RoundJoin);
painter->setPen(borderPen);
painter->setBrush(Qt::NoBrush); // 只画边框,不填充
painter->drawRoundedRect(rect, radius, radius);
边框颜色在 Step 2 中已经根据状态选好了。如果是 default 按钮(isDefault),边框用 accent 色;如果 disabled,用浅灰色。
注意:setPen 的线宽会影响圆角矩形的视觉大小。penW = 1.5 时,线条以矩形边界为中心向内外各扩展 0.75px。如果矩形正好是 widget 的 rect,边框有一半会被裁掉。实际工程中通常用 rect.adjusted(1, 1, -1, -1) 做内缩。
Step 5: Focus 指示框
// --- Step 5: Focus 焦点框 ---
if (hasFocus && !isPressed) {
QRectF focusRect = QRectF(rect).adjusted(
-theme.focusRingMargin,
-theme.focusRingMargin,
theme.focusRingMargin,
theme.focusRingMargin
);
QPen focusPen(theme.focusColor, theme.focusRingWidth);
focusPen.setStyle(Qt::DotLine); // 虚线效果
painter->setPen(focusPen);
painter->setBrush(Qt::NoBrush);
painter->drawRoundedRect(
focusRect,
radius + theme.focusRingMargin,
radius + theme.focusRingMargin
);
}
break; // PE_FrameButtonBevel
}
焦点框比按钮大一圈(focusRingMargin = 2),用虚线(DotLine)或实线,颜色与正文区分。pressed 状态下不画焦点框,避免视觉干扰。
1.3 完整 PE_FrameButtonBevel 代码
// ============================================================
// 文件: MiniStyle.cpp — drawPrimitive 部分
// ============================================================
#include "MiniStyle.hpp"
#include "MiniTheme.hpp"
void MiniStyle::drawPrimitive(
PrimitiveElement pe,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget
) const {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
switch (pe) {
// ==================== 按钮面板 ====================
case PE_FrameButtonBevel: {
drawFrameButtonBevel(option, painter, widget);
break;
}
// ==================== 面板(通用 Frame) ====================
case PE_Frame: {
const QRect r = option->rect;
MiniTheme t = MiniThemeManager::currentTheme();
painter->setPen(t.borderColor);
painter->setBrush(t.surfaceBg);
painter->drawRoundedRect(r, t.borderRadius, t.borderRadius);
break;
}
// ==================== 焦点框 ====================
case PE_FrameFocusRect: {
if (option->state & State_HasFocus) {
MiniTheme t = MiniThemeManager::currentTheme();
QPen pen(t.focusColor, t.focusRingWidth);
pen.setStyle(Qt::DotLine);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawRect(option->rect);
}
break;
}
// ==================== 输入框面板 ====================
case PE_PanelLineEdit: {
const QRect r = option->rect;
MiniTheme t = MiniThemeManager::currentTheme();
const bool hasFocus = option->state & State_HasFocus;
const bool enabled = option->state & State_Enabled;
QColor bg = enabled ? t.inputBg : t.inputDisabledBg;
QColor bd = hasFocus ? t.focusColor : t.borderColor;
qreal penW = hasFocus ? 2.0 : t.borderWidth;
painter->setPen(QPen(bd, penW));
painter->setBrush(bg);
painter->drawRoundedRect(r, t.borderRadius, t.borderRadius);
break;
}
// ==================== 指示箭头 ====================
case PE_IndicatorArrowDown:
case PE_IndicatorArrowUp:
case PE_IndicatorArrowLeft:
case PE_IndicatorArrowRight: {
drawArrow(pe, option, painter, widget);
break;
}
// ==================== CheckBox / RadioButton 选中指示 ====================
case PE_IndicatorCheckBox: {
drawCheckBoxIndicator(option, painter);
break;
}
case PE_IndicatorRadioButton: {
drawRadioButtonIndicator(option, painter);
break;
}
// ==================== 菜单/工具栏分隔线 ====================
case PE_IndicatorToolBarSeparator:
case PE_IndicatorMenuCheckMark: {
// 用 QCommonStyle 的默认绘制
break;
}
default:
break;
}
painter->restore();
}
// ----------------------------------------------------------
// PE_FrameButtonBevel 的独立函数实现
// ----------------------------------------------------------
void MiniStyle::drawFrameButtonBevel(
const QStyleOption *option,
QPainter *painter,
const QWidget *widget
) const {
const auto *opt = qstyleoption_cast<const QStyleOptionButton *>(option);
if (!opt) return;
const MiniTheme theme = MiniThemeManager::currentTheme();
// Step 1: 提取状态
const bool isHovered = opt->state & State_MouseOver;
const bool isPressed = opt->state & State_Sunken;
const bool isChecked = opt->state & State_On;
const bool isDisabled = !(opt->state & State_Enabled);
const bool isDefault = opt->features & QStyleOptionButton::DefaultButton;
const bool hasFocus = opt->state & State_HasFocus;
// Step 2: 查找颜色
const QColor bg = [&]() {
if (isDisabled) return theme.buttonDisabledBg;
if (isPressed) return theme.buttonPressedBg;
if (isChecked) return theme.buttonCheckedBg;
if (isHovered) return theme.buttonHoverBg;
return theme.buttonBg;
}();
const QColor borderColor = [&]() {
if (isDisabled) return theme.borderDisabledColor;
if (hasFocus) return theme.focusColor;
if (isDefault) return theme.accentColor;
return theme.borderColor;
}();
// Step 3: 填充背景
const QRectF r = opt->rect;
const qreal radius = theme.borderRadius;
painter->setPen(Qt::NoPen);
painter->setBrush(bg);
painter->drawRoundedRect(r, radius, radius);
// Step 4: 绘制边框
painter->setBrush(Qt::NoBrush);
{
QPen pen(borderColor, theme.borderWidth);
pen.setJoinStyle(Qt::RoundJoin);
painter->setPen(pen);
}
painter->drawRoundedRect(r, radius, radius);
// Step 5: Focus 指示框
if (hasFocus && !isPressed) {
const qreal margin = theme.focusRingMargin;
QRectF focusRect = r.adjusted(-margin, -margin, margin, margin);
QPen fPen(theme.focusColor, theme.focusRingWidth);
fPen.setStyle(Qt::DotLine);
painter->setPen(fPen);
painter->drawRoundedRect(
focusRect,
radius + margin,
radius + margin
);
}
}
1.4 其他常用 PrimitiveElement
| 枚举值 | 绘制内容 | 关键 QStyleOption |
|---|---|---|
PE_Frame |
通用圆角面板 | option->rect + palette |
PE_FrameFocusRect |
虚线焦点框 | State_HasFocus |
PE_PanelLineEdit |
输入框背景 | 同 Frame,多了 active 边框 |
PE_IndicatorArrowDown/Up/Left/Right |
三角形箭头 | option->rect 内居中画三角 |
PE_IndicatorCheckBox |
复选框勾号 | State_On, State_NoChange |
PE_IndicatorRadioButton |
单选框圆点 | State_On |
PE_IndicatorMenuCheckMark |
菜单中的勾 | 默认实现即可 |
PE_IndicatorToolBarSeparator |
工具栏竖线 | 1px 竖线 |
2. drawControl 深度解析:CE_PushButton
drawControl 比 drawPrimitive 高一层。它负责整个"逻辑控件"的绘制,内部会调用多次 drawPrimitive。
2.1 CE_PushButton 的绘制层次
Qt Framework 对 QPushButton 的 paintEvent 大致走以下路径:
QPushButton::paintEvent()
└─ QStyle::drawControl(CE_PushButton, ...)
├─ drawControl(CE_PushButtonBevel) // 按钮面板
│ └─ drawPrimitive(PE_FrameButtonBevel) // → 圆角矩形
└─ drawControl(CE_PushButtonLabel) // 按钮上的文字+图标
├─ drawItemText() // → 文字
├─ drawItemPixmap() // → 图标
└─ drawPrimitive(PE_IndicatorArrowDown) // → 菜单箭头
注意:CE_PushButtonBevel 和 PE_FrameButtonBevel 是两个不同的枚举。CE_PushButtonBevel 是 control 层,它内部再调用 PE_FrameButtonBevel。你可以在 CE_PushButtonBevel 层做额外的状态判断(比如根据 button 的 position 决定是否合并边框),再委托给 primitive。
在简单实现中,我们通常直接把 CE_PushButtonBevel 转发到 drawPrimitive(PE_FrameButtonBevel, ...)。
2.2 完整 CE_PushButton 实现
// ============================================================
// 文件: MiniStyle.cpp — drawControl 部分
// ============================================================
void MiniStyle::drawControl(
ControlElement ce,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget
) const {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
switch (ce) {
case CE_PushButton: {
drawPushButton(option, painter, widget);
break;
}
case CE_PushButtonBevel: {
// 委托给 primitive 层
drawPrimitive(PE_FrameButtonBevel, option, painter, widget);
break;
}
case CE_PushButtonLabel: {
drawPushButtonLabel(option, painter, widget);
break;
}
case CE_TabBarTab: {
drawTabBarTab(option, painter, widget);
break;
}
case CE_ProgressBar: {
drawProgressBar(option, painter, widget);
break;
}
default:
// 委托给 QCommonStyle 的默认实现
break;
}
painter->restore();
}
// ==================================================================
// CE_PushButton 完整实现
// ==================================================================
void MiniStyle::drawPushButton(
const QStyleOption *option,
QPainter *painter,
const QWidget *widget
) const {
const auto *opt = qstyleoption_cast<const QStyleOptionButton *>(option);
if (!opt) return;
const MiniTheme theme = MiniThemeManager::currentTheme();
// 1. 绘制按钮面板 (CE_PushButtonBevel → PE_FrameButtonBevel)
drawControl(CE_PushButtonBevel, option, painter, widget);
// 2. 绘制文字 + 图标 (CE_PushButtonLabel)
// 内部需要根据是否 disabled 调整颜色,所以直接在这里处理
QStyleOptionButton subOpt = *opt;
// disabled 状态下文字变灰
if (!(opt->state & State_Enabled)) {
subOpt.palette.setColor(QPalette::ButtonText,
theme.textDisabledColor);
}
drawControl(CE_PushButtonLabel, &subOpt, painter, widget);
}
// ==================================================================
// CE_PushButtonLabel — 图标 + 文字布局与绘制
// ==================================================================
void MiniStyle::drawPushButtonLabel(
const QStyleOption *option,
QPainter *painter,
const QWidget *widget
) const {
const auto *opt = qstyleoption_cast<const QStyleOptionButton *>(option);
if (!opt) return;
const MiniTheme theme = MiniThemeManager::currentTheme();
QRect contentRect = opt->rect;
const bool hasText = !opt->text.isEmpty();
const bool hasIcon = !opt->icon.isNull();
const bool hasMenu = (opt->features & QStyleOptionButton::HasMenu);
// --- 边距 ---
const int hmargin = theme.buttonPaddingH; // e.g. 16
const int vmargin = theme.buttonPaddingV; // e.g. 6
const int spacing = theme.buttonIconSpacing; // e.g. 8
contentRect.adjust(hmargin, vmargin, -hmargin, -vmargin);
// --- 菜单箭头占用右侧空间 ---
QRect menuArrowRect;
if (hasMenu) {
const int arrowSize = theme.arrowSize; // e.g. 12
menuArrowRect = QRect(
contentRect.right() - arrowSize - spacing,
contentRect.center().y() - arrowSize / 2,
arrowSize, arrowSize
);
contentRect.setRight(menuArrowRect.left() - spacing);
}
// --- 计算图标区域 ---
QRect iconRect;
if (hasIcon) {
const QSize iconSize = opt->iconSize.isValid()
? opt->iconSize
: QSize(theme.iconSize, theme.iconSize);
iconRect = QRect(
contentRect.left(),
contentRect.center().y() - iconSize.height() / 2,
iconSize.width(), iconSize.height()
);
contentRect.setLeft(iconRect.right() + spacing);
}
// --- 计算对齐方式 ---
const int alignment = Qt::AlignVCenter |
(hasIcon ? Qt::AlignLeft : Qt::AlignCenter);
// --- 绘制文字 ---
if (hasText) {
painter->setPen(opt->palette.color(QPalette::ButtonText));
painter->setFont(opt->font);
// 文字过长时省略号
const QString elidedText = painter->fontMetrics().elidedText(
opt->text, Qt::ElideRight, contentRect.width()
);
drawItemText(painter, contentRect, alignment, opt->palette,
true, elidedText, QPalette::ButtonText);
}
// --- 绘制图标 ---
if (hasIcon) {
const QIcon::Mode mode = (opt->state & State_Enabled)
? (opt->state & State_Sunken ? QIcon::Active : QIcon::Normal)
: QIcon::Disabled;
const QIcon::State iconState = (opt->state & State_On)
? QIcon::On : QIcon::Off;
const QPixmap pixmap = opt->icon.pixmap(
opt->iconSize.isValid() ? opt->iconSize
: QSize(theme.iconSize, theme.iconSize),
mode, iconState
);
drawItemPixmap(painter, iconRect, Qt::AlignCenter, pixmap);
}
// --- 绘制菜单箭头 ---
if (hasMenu) {
QStyleOption arrowOpt;
arrowOpt.rect = menuArrowRect;
arrowOpt.state = opt->state;
arrowOpt.palette = opt->palette;
drawPrimitive(PE_IndicatorArrowDown, &arrowOpt, painter, widget);
}
}
2.3 QIcon::Mode 和 QIcon::State 的映射
这是很多初学者栽坑的地方。QIcon 支持四种模式和两种状态:
enum Mode { Normal, Disabled, Active, Selected };
enum State { On, Off };
从 QStyleOption 到 QIcon 参数的映射规则:
| QStyleOption State | QIcon::Mode | QIcon::State |
|---|---|---|
| Enabled + no hover | Normal | Off |
| Enabled + hover | Active | Off |
| Enabled + pressed | Active | On |
| Disabled | Disabled | Off |
| Enabled + checked (toggle button) | Normal | On |
| Enabled + checked + hover | Active | On |
QIcon::pixmap() 会根据 mode 自动选择对应版本的图标(通常 normal 是彩色,disabled 是灰色,active 是亮色)。
2.4 drawControl 完整 switch 清单参考
void MiniStyle::drawControl(ControlElement ce, ...) const {
switch (ce) {
case CE_PushButton: drawPushButton(...); break;
case CE_PushButtonBevel: drawPrimitive(PE_FrameButtonBevel, ...); break;
case CE_PushButtonLabel: drawPushButtonLabel(...); break;
case CE_TabBarTab: drawTabBarTab(...); break;
case CE_ProgressBar: drawProgressBar(...); break;
case CE_ProgressBarContents: drawProgressBarContents(...); break;
case CE_ProgressBarLabel: drawProgressBarLabel(...); break;
case CE_MenuBarItem: drawMenuBarItem(...); break;
case CE_MenuItem: drawMenuItem(...); break;
case CE_CheckBox: drawCheckBox(...); break;
case CE_RadioButton: drawRadioButton(...); break;
case CE_ComboBoxLabel: drawComboBoxLabel(...); break;
case CE_HeaderSection: drawHeaderSection(...); break;
case CE_ToolButtonLabel: drawToolButtonLabel(...); break;
default: break;
}
}
3. drawComplexControl 实战:CC_ScrollBar
CC_ScrollBar 是典型的多子控件复合体。它由一个凹槽(groove)、一个滑块(slider)和两个箭头按钮(subLine/addLine)组成。
3.1 子控件布局:subControlRect
Qt 通过 subControlRect() 告诉你每个子控件的矩形区域。你必须覆盖这个函数,否则子控件的位置就是默认的(通常不是你想要的)。
QRect MiniStyle::subControlRect(
ComplexControl cc,
const QStyleOptionComplex *opt,
SubControl sc,
const QWidget *widget
) const {
switch (cc) {
case CC_ScrollBar: {
const auto *sbOpt = qstyleoption_cast<const QStyleOptionSlider *>(opt);
const QRect scrollBarRect = sbOpt->rect;
const MiniTheme theme = MiniThemeManager::currentTheme();
const int arrowSize = theme.scrollBarArrowSize; // e.g. 16
const bool horizontal = sbOpt->orientation == Qt::Horizontal;
int sliderMin, sliderMax, sliderLen;
int startPos, endPos;
if (horizontal) {
startPos = scrollBarRect.left() + arrowSize;
endPos = scrollBarRect.right() - arrowSize;
sliderMin = scrollBarRect.top();
sliderMax = scrollBarRect.bottom();
sliderLen = std::max(theme.scrollBarMinSliderLen,
static_cast<int>(sbOpt->pageStep * (endPos - startPos)
/ (qreal)(sbOpt->maximum - sbOpt->minimum + sbOpt->pageStep)));
} else {
startPos = scrollBarRect.top() + arrowSize;
endPos = scrollBarRect.bottom() - arrowSize;
sliderMin = scrollBarRect.left();
sliderMax = scrollBarRect.right();
sliderLen = std::max(theme.scrollBarMinSliderLen,
static_cast<int>(sbOpt->pageStep * (endPos - startPos)
/ (qreal)(sbOpt->maximum - sbOpt->minimum + sbOpt->pageStep)));
}
// 滑块位置
const int range = sbOpt->maximum - sbOpt->minimum;
const int sliderStart = (range > 0)
? startPos + (endPos - startPos - sliderLen) * sbOpt->sliderValue / range
: startPos;
switch (sc) {
case SC_ScrollBarGroove:
// 凹槽 = 整个滚动条区域
return scrollBarRect;
case SC_ScrollBarSlider:
if (horizontal)
return QRect(sliderStart, sliderMin, sliderLen,
sliderMax - sliderMin);
else
return QRect(sliderMin, sliderStart,
sliderMax - sliderMin, sliderLen);
case SC_ScrollBarSubLine:
if (horizontal)
return QRect(scrollBarRect.left(), scrollBarRect.top(),
arrowSize, scrollBarRect.height());
else
return QRect(scrollBarRect.left(), scrollBarRect.top(),
scrollBarRect.width(), arrowSize);
case SC_ScrollBarAddLine:
if (horizontal)
return QRect(scrollBarRect.right() - arrowSize,
scrollBarRect.top(), arrowSize, scrollBarRect.height());
else
return QRect(scrollBarRect.left(),
scrollBarRect.bottom() - arrowSize,
scrollBarRect.width(), arrowSize);
default:
break;
}
break;
}
// ... CC_ComboBox, CC_SpinBox 等 ...
default:
break;
}
return QCommonStyle::subControlRect(cc, opt, sc, widget);
}
3.2 完整 CC_ScrollBar 绘制实现
void MiniStyle::drawComplexControl(
ComplexControl cc,
const QStyleOptionComplex *option,
QPainter *painter,
const QWidget *widget
) const {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
switch (cc) {
case CC_ScrollBar: {
drawScrollBar(option, painter, widget);
break;
}
case CC_ComboBox: {
drawComboBox(option, painter, widget);
break;
}
case CC_SpinBox: {
drawSpinBox(option, painter, widget);
break;
}
default:
break;
}
painter->restore();
}
// ==================================================================
// CC_ScrollBar 完整绘制
// ==================================================================
void MiniStyle::drawScrollBar(
const QStyleOptionComplex *option,
QPainter *painter,
const QWidget *widget
) const {
const auto *opt = qstyleoption_cast<const QStyleOptionSlider *>(option);
if (!opt) return;
const MiniTheme theme = MiniThemeManager::currentTheme();
const bool horizontal = opt->orientation == Qt::Horizontal;
const bool isHovered = opt->state & State_MouseOver;
// --- 子控件矩形 ---
const QRect grooveRect = subControlRect(CC_ScrollBar, opt, SC_ScrollBarGroove, widget);
const QRect sliderRect = subControlRect(CC_ScrollBar, opt, SC_ScrollBarSlider, widget);
const QRect subLineRect = subControlRect(CC_ScrollBar, opt, SC_ScrollBarSubLine, widget);
const QRect addLineRect = subControlRect(CC_ScrollBar, opt, SC_ScrollBarAddLine, widget);
// 1. 绘制凹槽
{
painter->setPen(Qt::NoPen);
painter->setBrush(theme.scrollBarGrooveBg);
painter->drawRoundedRect(grooveRect, theme.scrollBarRadius,
theme.scrollBarRadius);
}
// 2. 绘制滑块
if (opt->minimum < opt->maximum) {
QColor sliderBg = opt->activeSubControls & SC_ScrollBarSlider
? theme.scrollBarSliderActiveBg
: theme.scrollBarSliderBg;
painter->setPen(Qt::NoPen);
painter->setBrush(sliderBg);
painter->drawRoundedRect(sliderRect, theme.scrollBarRadius,
theme.scrollBarRadius);
}
// 3. 绘制箭头按钮
const QColor arrowColor = opt->palette.color(QPalette::ButtonText);
// SubLine(上/左箭头)
{
QStyleOption arrowOpt;
arrowOpt.rect = subLineRect;
arrowOpt.state = opt->state;
arrowOpt.palette = opt->palette;
// 按钮背景
if (opt->activeSubControls & SC_ScrollBarSubLine) {
painter->setPen(Qt::NoPen);
painter->setBrush(theme.scrollBarArrowHoverBg);
painter->drawRoundedRect(subLineRect, theme.scrollBarRadius,
theme.scrollBarRadius);
}
drawPrimitive(horizontal ? PE_IndicatorArrowLeft
: PE_IndicatorArrowUp,
&arrowOpt, painter, widget);
}
// AddLine(下/右箭头)
{
QStyleOption arrowOpt;
arrowOpt.rect = addLineRect;
arrowOpt.state = opt->state;
arrowOpt.palette = opt->palette;
if (opt->activeSubControls & SC_ScrollBarAddLine) {
painter->setPen(Qt::NoPen);
painter->setBrush(theme.scrollBarArrowHoverBg);
painter->drawRoundedRect(addLineRect, theme.scrollBarRadius,
theme.scrollBarRadius);
}
drawPrimitive(horizontal ? PE_IndicatorArrowRight
: PE_IndicatorArrowDown,
&arrowOpt, painter, widget);
}
}
3.3 activeSubControls 的魔力
QStyleOptionComplex::activeSubControls 是一个位掩码,Qt 框架层在鼠标 hover 时已经设置好了。你只需要:
if (opt->activeSubControls & SC_ScrollBarSlider) {
// 鼠标正在滑块上 → 高亮绘制
}
不需要自己写 widget->mapFromGlobal(QCursor::pos()) 那套逻辑。这是 QStyle 系统最体贴的设计之一。
4. 绘制原语工具箱
在实际工程中,你不会只在 drawPrimitive 里写几十个 case。大量的绘制逻辑应该抽成自由函数,放在独立的工具文件中。这样 drawPrimitive 保持干净,工具函数可以在任何地方复用。
4.1 工具函数头文件
// ============================================================
// 文件: PrimitiveUtils.hpp
// ============================================================
#pragma once
#include <QPainter>
#include <QPixmap>
#include <QPixmapCache>
#include <QIcon>
#include <QColor>
#include <QRectF>
#include <QString>
struct MiniTheme; // 前向声明
// --- 核心绘制 ---
void drawRoundedRect(
QPainter *painter,
const QRectF &rect,
const QColor &fillColor,
const QColor &borderColor,
qreal radius,
qreal borderWidth
);
void drawRoundedRectBorder(
QPainter *painter,
const QRectF &rect,
const QColor &borderColor,
qreal radius,
qreal borderWidth
);
// --- 图标 ---
QPixmap getPixmap(
const QIcon &icon,
const QSize &size,
QIcon::Mode mode = QIcon::Normal,
QIcon::State state = QIcon::Off
);
QPixmap colorizePixmap(
const QPixmap &source,
const QColor &color
);
// --- 徽章 ---
void drawBadge(
QPainter *painter,
const QRectF &rect,
const QString &text,
const QColor &bgColor,
const QColor &textColor
);
// --- 箭头 ---
void drawArrow(
QPainter *painter,
const QRectF &rect,
const QColor &color,
Qt::ArrowType direction,
qreal arrowSize = -1 // -1 = auto size based on rect
);
// --- Focus 框 ---
void drawFocusRing(
QPainter *painter,
const QRectF &rect,
const QColor &color,
qreal width,
qreal margin
);
4.2 drawRoundedRect — 填色 + 边框 + Focus 三合一
// ============================================================
// 文件: PrimitiveUtils.cpp
// ============================================================
#include "PrimitiveUtils.hpp"
#include <QPainterPath>
void drawRoundedRect(
QPainter *painter,
const QRectF &rect,
const QColor &fillColor,
const QColor &borderColor,
qreal radius,
qreal borderWidth
) {
painter->save();
// 如果 borderWidth > 0,内缩一半线宽避免被裁切
const qreal halfPen = borderWidth * 0.5;
QRectF adjustedRect = rect.adjusted(halfPen, halfPen, -halfPen, -halfPen);
if (borderWidth > 0 && borderColor.alpha() > 0) {
// 有边框:用 QPainterPath 分别填充和描边
QPainterPath path;
path.addRoundedRect(adjustedRect, radius, radius);
painter->setPen(Qt::NoPen);
painter->setBrush(fillColor);
painter->drawPath(path);
QPen borderPen(borderColor, borderWidth);
borderPen.setJoinStyle(Qt::RoundJoin);
painter->setPen(borderPen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(path);
} else {
// 无边框:只填充
painter->setPen(Qt::NoPen);
painter->setBrush(fillColor);
painter->drawRoundedRect(adjustedRect, radius, radius);
}
painter->restore();
}
使用 QPainterPath + 同一条 path 的好处:填充和描边完美对齐,不会出现 0.5px 偏移。
4.3 drawRoundedRectBorder — 仅边框
void drawRoundedRectBorder(
QPainter *painter,
const QRectF &rect,
const QColor &borderColor,
qreal radius,
qreal borderWidth
) {
painter->save();
const qreal halfPen = borderWidth * 0.5;
QRectF adjustedRect = rect.adjusted(halfPen, halfPen, -halfPen, -halfPen);
QPen pen(borderColor, borderWidth);
pen.setJoinStyle(Qt::RoundJoin);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawRoundedRect(adjustedRect, radius, radius);
painter->restore();
}
4.4 getPixmap — QPixmapCache 缓存
Qt 的 QIcon::pixmap() 每次调用都会重新渲染矢量图标(如果是 SVG),在列表/树控件中可能触发严重性能问题。QPixmapCache 可以大幅降低重复渲染开销。
QPixmap getPixmap(
const QIcon &icon,
const QSize &size,
QIcon::Mode mode,
QIcon::State state
) {
if (icon.isNull()) {
return QPixmap();
}
// 缓存键:icon cacheKey + size + mode + state
const QString key = QString("icon_%1_%2x%3_%4_%5")
.arg(icon.cacheKey())
.arg(size.width())
.arg(size.height())
.arg(static_cast<int>(mode))
.arg(static_cast<int>(state));
QPixmap cached;
if (QPixmapCache::find(key, &cached)) {
return cached;
}
QPixmap pixmap = icon.pixmap(size, mode, state);
QPixmapCache::insert(key, pixmap);
return pixmap;
}
性能提示:
QPixmapCache::setCacheLimit()默认 10240 KB。如果你的应用有大量图标(如文件管理器),可以适当调大。
4.5 colorizePixmap — 图标染色
很多设计系统需要对单色 SVG 图标做色调变换(hover 时变蓝,disabled 时变灰)。这就是 colorizePixmap 的用途:
QPixmap colorizePixmap(
const QPixmap &source,
const QColor &color
) {
if (source.isNull()) return QPixmap();
QPixmap result = source;
QPainter p(&result);
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.fillRect(result.rect(), color);
p.end();
return result;
}
原理:CompositionMode_SourceIn 保留源图像(图标)的 alpha 通道,但把 RGB 替换为目标颜色。对于单色 SVG 图标,这等于"染了一个新颜色”。
联动 getPixmap:通常的做法是缓存原图,染色不缓存(染色非常快)。如需缓存染色结果,同样可以走 QPixmapCache:
QPixmap getPixmapColored(
const QIcon &icon,
const QSize &size,
const QColor &color,
QIcon::Mode mode = QIcon::Normal,
QIcon::State state = QIcon::Off
) {
const QString key = QString("icon_colored_%1_%2x%3_%4_%5_%6")
.arg(icon.cacheKey())
.arg(size.width()).arg(size.height())
.arg(static_cast<int>(mode))
.arg(static_cast<int>(state))
.arg(color.name(QColor::HexArgb));
QPixmap cached;
if (QPixmapCache::find(key, &cached)) {
return cached;
}
QPixmap base = getPixmap(icon, size, mode, state);
QPixmap colored = colorizePixmap(base, color);
QPixmapCache::insert(key, colored);
return colored;
}
4.6 drawBadge — 通知徽章
void drawBadge(
QPainter *painter,
const QRectF &rect,
const QString &text,
const QColor &bgColor,
const QColor &textColor
) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
// 圆形徽章
const qreal radius = qMin(rect.width(), rect.height()) / 2.0;
QRectF circleRect(
rect.center().x() - radius,
rect.center().y() - radius,
radius * 2, radius * 2
);
painter->setPen(Qt::NoPen);
painter->setBrush(bgColor);
painter->drawEllipse(circleRect);
// 文字
if (!text.isEmpty()) {
painter->setPen(textColor);
QFont font = painter->font();
font.setPixelSize(static_cast<int>(radius * 1.2));
font.setBold(true);
painter->setFont(font);
painter->drawText(circleRect, Qt::AlignCenter, text);
}
painter->restore();
}
4.7 drawArrow — 三角形箭头
void drawArrow(
QPainter *painter,
const QRectF &rect,
const QColor &color,
Qt::ArrowType direction,
qreal arrowSize
) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
if (arrowSize <= 0) {
arrowSize = qMin(rect.width(), rect.height()) * 0.4;
}
// 计算中心
const QPointF center = rect.center();
// 构建三角形路径
QPainterPath path;
switch (direction) {
case Qt::DownArrow:
path.moveTo(center.x() - arrowSize, center.y() - arrowSize * 0.35);
path.lineTo(center.x() + arrowSize, center.y() - arrowSize * 0.35);
path.lineTo(center.x(), center.y() + arrowSize * 0.65);
break;
case Qt::UpArrow:
path.moveTo(center.x() - arrowSize, center.y() + arrowSize * 0.35);
path.lineTo(center.x() + arrowSize, center.y() + arrowSize * 0.35);
path.lineTo(center.x(), center.y() - arrowSize * 0.65);
break;
case Qt::LeftArrow:
path.moveTo(center.x() + arrowSize * 0.35, center.y() - arrowSize);
path.lineTo(center.x() + arrowSize * 0.35, center.y() + arrowSize);
path.lineTo(center.x() - arrowSize * 0.65, center.y());
break;
case Qt::RightArrow:
path.moveTo(center.x() - arrowSize * 0.35, center.y() - arrowSize);
path.lineTo(center.x() - arrowSize * 0.35, center.y() + arrowSize);
path.lineTo(center.x() + arrowSize * 0.65, center.y());
break;
default:
break;
}
path.closeSubpath();
painter->setPen(Qt::NoPen);
painter->setBrush(color);
painter->drawPath(path);
painter->restore();
}
5. sizeFromContents / pixelMetric / styleHint
绘制之外,QStyle 还负责告诉 Qt 控件应该有多大。三个关键虚拟函数:
5.1 sizeFromContents — 控件尺寸计算
Qt 在 layout 阶段调用 sizeFromContents,你返回 QSize。Qt 用自己的 layout 规则把这个 size 嵌入布局系统。
QSize MiniStyle::sizeFromContents(
ContentsType ct,
const QStyleOption *opt,
const QSize &contentsSize,
const QWidget *widget
) const {
const MiniTheme theme = MiniThemeManager::currentTheme();
switch (ct) {
case CT_PushButton: {
const auto *btn = qstyleoption_cast<const QStyleOptionButton *>(opt);
if (!btn) break;
// contentSize 是 Qt 根据文字 + 图标算出的最小内容尺寸
const int hmargin = theme.buttonPaddingH * 2; // 左右各 padding
const int vmargin = theme.buttonPaddingV * 2; // 上下各 padding
const int minW = theme.buttonMinWidth; // e.g. 80
const int minH = theme.buttonMinHeight; // e.g. 30
return QSize(
qMax(contentsSize.width() + hmargin, minW),
qMax(contentsSize.height() + vmargin, minH)
);
}
case CT_TabBarTab: {
const auto *tab = qstyleoption_cast<const QStyleOptionTab *>(opt);
if (!tab) break;
const int hmargin = theme.tabPaddingH * 2;
const int vmargin = theme.tabPaddingV * 2;
const int minW = theme.tabMinWidth;
return QSize(
qMax(contentsSize.width() + hmargin, minW),
qMax(contentsSize.height() + vmargin, theme.tabHeight)
);
}
case CT_LineEdit: {
return QSize(
contentsSize.width() + theme.inputPaddingH * 2,
qMax(contentsSize.height() + theme.inputPaddingV * 2,
theme.inputMinHeight)
);
}
case CT_Slider: {
const auto *slider = qstyleoption_cast<const QStyleOptionSlider *>(opt);
if (!slider) break;
if (slider->orientation == Qt::Horizontal) {
return QSize(contentsSize.width(), theme.sliderHeight);
} else {
return QSize(theme.sliderWidth, contentsSize.height());
}
}
case CT_ComboBox: {
return QSize(
contentsSize.width() + theme.comboBoxPaddingH * 2 + theme.arrowSize + 8,
qMax(contentsSize.height() + theme.comboBoxPaddingV * 2,
theme.inputMinHeight)
);
}
case CT_CheckBox:
case CT_RadioButton: {
const int indicator = theme.checkBoxSize; // e.g. 18
const int spacing = theme.checkBoxSpacing; // e.g. 6
return QSize(
contentsSize.width() + indicator + spacing + 4,
qMax(contentsSize.height(), indicator)
);
}
default:
break;
}
return QCommonStyle::sizeFromContents(ct, opt, contentsSize, widget);
}
核心原理:Qt 先算出 contentsSize(文字宽度 + 图标大小 + 间距),然后你在此基础上加 padding,再和 minWidth / minHeight 取 max。这给了你完全的尺寸控制权。
5.2 pixelMetric — 像素级常量
int MiniStyle::pixelMetric(
PixelMetric pm,
const QStyleOption *opt,
const QWidget *widget
) const {
const MiniTheme theme = MiniThemeManager::currentTheme();
switch (pm) {
// --- 图标 ---
case PM_SmallIconSize: return 16;
case PM_ButtonIconSize: return theme.iconSize; // 20
case PM_LargeIconSize: return 32;
case PM_ToolBarIconSize: return 22;
// --- 按钮 ---
case PM_ButtonMargin: return theme.buttonPaddingH; // 16
case PM_DefaultFrameWidth: return static_cast<int>(theme.borderWidth);
// --- 菜单 ---
case PM_MenuHMargin: return 4;
case PM_MenuVMargin: return 4;
case PM_MenuBarHMargin: return 0;
case PM_MenuBarVMargin: return 0;
case PM_MenuBarItemSpacing: return 4;
// --- Tab ---
case PM_TabBarTabHSpace: return theme.tabPaddingH * 2;
case PM_TabBarTabVSpace: return theme.tabPaddingV * 2;
case PM_TabBarBaseHeight: return 0; // 无底部线
// --- 滚动条 ---
case PM_ScrollBarExtent: return theme.scrollBarWidth; // 8
case PM_ScrollBarSliderMin: return theme.scrollBarMinSliderLen; // 30
// --- 滑块 ---
case PM_SliderThickness: return theme.sliderHeight;
case PM_SliderLength: return theme.sliderHandleSize;
case PM_SliderTickmarkOffset:return 4;
// --- CheckBox / Radio ---
case PM_IndicatorWidth: return theme.checkBoxSize;
case PM_IndicatorHeight: return theme.checkBoxSize;
case PM_CheckBoxLabelSpacing: return theme.checkBoxSpacing;
// --- 通用 ---
case PM_LayoutLeftMargin: return 9;
case PM_LayoutTopMargin: return 9;
case PM_LayoutRightMargin: return 9;
case PM_LayoutBottomMargin: return 9;
default:
break;
}
return QCommonStyle::pixelMetric(pm, opt, widget);
}
pixelMetric 和 sizeFromContents 之间的分工:
pixelMetric:返回单个像素值(图标大小、间距、线宽)sizeFromContents:返回完整控件尺寸(QSize)sizeFromContents内部通常会调用pixelMetric来获取 padding/spacing
5.3 styleHint — 行为提示
int MiniStyle::styleHint(
StyleHint sh,
const QStyleOption *opt,
const QWidget *widget,
QStyleHintReturn *returnData
) const {
switch (sh) {
// --- 右键菜单弹出位置 ---
case SH_MenuBar_AltKeyNavigation:
return 1;
// --- 菜单是否可以拖动分离 ---
case SH_MenuBar_DismissOnSecondClick:
return 1;
// --- 下拉菜单是否有延迟(hover 后才打开) ---
case SH_Menu_SubMenuPopupDelay:
return 160; // ms
// --- 右键菜单出现位置(屏幕坐标)---
case SH_Menu_Scrollable:
return 1;
// --- Tab 是否可以滚动 ---
case SH_TabBar_Alignment:
return Qt::AlignLeft;
// --- Slider 点击时跳转到该位置 ---
case SH_Slider_AbsoluteSetButtons:
return Qt::LeftButton;
// --- 输入框选中文字时能否右键复制 ---
case SH_LineEdit_PasswordCharacter:
return 0x25CF; // ●
// --- Focus 的绘制策略 ---
case SH_UnderlineShortcut:
return (opt && (opt->state & State_Enabled)) ? 1 : 0;
// --- 动画 ---
case SH_Widget_Animation_Duration:
return theme.animationDuration; // e.g. 200
// --- 滚动条点击策略 ---
case SH_ScrollBar_LeftClickAbsolutePosition:
return 0; // 点击轨道不跳转,只按 pageStep 翻页
default:
break;
}
return QCommonStyle::styleHint(sh, opt, widget, returnData);
}
styleHint 控制的是行为而非外观。比如 SH_Menu_SubMenuPopupDelay 决定鼠标 hover 子菜单后多少毫秒弹出。这些 hint 的数量有上百个,你不需要全改——只改和你的设计语言相关的即可。
6. 实战练习:完整 MiniStyle 按钮渲染
把前面所有的知识组合成一份可编译可运行的完整代码。
6.1 头文件
// ============================================================
// 文件: MiniStyle.hpp
// ============================================================
#pragma once
#include <QCommonStyle>
#include <QStyleOption>
#include <QPainter>
#include <QWidget>
class MiniStyle : 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;
void drawComplexControl(ComplexControl cc,
const QStyleOptionComplex *opt, QPainter *p,
const QWidget *w = nullptr) const override;
// --- 子控件布局 ---
QRect subControlRect(ComplexControl cc,
const QStyleOptionComplex *opt, SubControl sc,
const QWidget *w = nullptr) const override;
// --- 尺寸与度量 ---
QSize sizeFromContents(ContentsType ct,
const QStyleOption *opt, const QSize &contentsSize,
const QWidget *w = nullptr) const override;
int pixelMetric(PixelMetric pm,
const QStyleOption *opt = nullptr,
const QWidget *w = nullptr) const override;
int styleHint(StyleHint sh,
const QStyleOption *opt = nullptr,
const QWidget *w = nullptr,
QStyleHintReturn *returnData = nullptr) const override;
private:
// --- PE 辅助 ---
void drawFrameButtonBevel(const QStyleOption *, QPainter *, const QWidget *) const;
// --- CE 辅助 ---
void drawPushButton(const QStyleOption *, QPainter *, const QWidget *) const;
void drawPushButtonLabel(const QStyleOption *, QPainter *, const QWidget *) const;
// --- CC 辅助 ---
void drawScrollBar(const QStyleOptionComplex *, QPainter *, const QWidget *) const;
};
6.2 实现文件(核心部分汇总)
// ============================================================
// 文件: MiniStyle.cpp
// ============================================================
#include "MiniStyle.hpp"
#include "MiniTheme.hpp"
// ==================== drawPrimitive ====================
void MiniStyle::drawPrimitive(
PrimitiveElement pe, const QStyleOption *opt,
QPainter *p, const QWidget *w
) const {
p->save();
p->setRenderHint(QPainter::Antialiasing);
switch (pe) {
case PE_FrameButtonBevel:
drawFrameButtonBevel(opt, p, w);
break;
case PE_PanelLineEdit: {
const QRect r = opt->rect;
auto t = MiniThemeManager::currentTheme();
bool focus = opt->state & State_HasFocus;
bool enabled = opt->state & State_Enabled;
p->setPen(QPen(focus ? t.focusColor : t.borderColor,
focus ? 2.0 : t.borderWidth));
p->setBrush(enabled ? t.inputBg : t.inputDisabledBg);
p->drawRoundedRect(r, t.borderRadius, t.borderRadius);
break;
}
default:
break;
}
p->restore();
}
// ==================== drawControl ====================
void MiniStyle::drawControl(
ControlElement ce, const QStyleOption *opt,
QPainter *p, const QWidget *w
) const {
p->save();
p->setRenderHint(QPainter::Antialiasing);
switch (ce) {
case CE_PushButton:
drawPushButton(opt, p, w);
break;
case CE_PushButtonBevel:
drawPrimitive(PE_FrameButtonBevel, opt, p, w);
break;
case CE_PushButtonLabel:
drawPushButtonLabel(opt, p, w);
break;
default:
break;
}
p->restore();
}
// ==================== drawComplexControl ====================
void MiniStyle::drawComplexControl(
ComplexControl cc, const QStyleOptionComplex *opt,
QPainter *p, const QWidget *w
) const {
p->save();
p->setRenderHint(QPainter::Antialiasing);
switch (cc) {
case CC_ScrollBar:
drawScrollBar(opt, p, w);
break;
default:
break;
}
p->restore();
}
// ==================== drawFrameButtonBevel ====================
void MiniStyle::drawFrameButtonBevel(
const QStyleOption *opt, QPainter *p, const QWidget *w
) const {
const auto *btn = qstyleoption_cast<const QStyleOptionButton *>(opt);
if (!btn) return;
auto t = MiniThemeManager::currentTheme();
bool hover = btn->state & State_MouseOver;
bool pressed= btn->state & State_Sunken;
bool checked= btn->state & State_On;
bool dis = !(btn->state & State_Enabled);
bool def = btn->features & QStyleOptionButton::DefaultButton;
bool focus = btn->state & State_HasFocus;
QColor bg = dis ? t.buttonDisabledBg
: pressed ? t.buttonPressedBg
: checked ? t.buttonCheckedBg
: hover ? t.buttonHoverBg
: t.buttonBg;
QColor bd = dis ? t.borderDisabledColor
: focus ? t.focusColor
: def ? t.accentColor
: t.borderColor;
QRectF r = btn->rect;
qreal rad = t.borderRadius;
// fill
p->setPen(Qt::NoPen);
p->setBrush(bg);
p->drawRoundedRect(r, rad, rad);
// border
p->setBrush(Qt::NoBrush);
QPen pen(bd, t.borderWidth);
pen.setJoinStyle(Qt::RoundJoin);
p->setPen(pen);
p->drawRoundedRect(r, rad, rad);
// focus ring
if (focus && !pressed) {
qreal m = t.focusRingMargin;
QRectF fr = r.adjusted(-m, -m, m, m);
QPen fp(t.focusColor, t.focusRingWidth);
fp.setStyle(Qt::DotLine);
p->setPen(fp);
p->drawRoundedRect(fr, rad + m, rad + m);
}
}
// ==================== drawPushButton ====================
void MiniStyle::drawPushButton(
const QStyleOption *opt, QPainter *p, const QWidget *w
) const {
const auto *btn = qstyleoption_cast<const QStyleOptionButton *>(opt);
if (!btn) return;
auto t = MiniThemeManager::currentTheme();
// 1. bevel
drawControl(CE_PushButtonBevel, opt, p, w);
// 2. label
QStyleOptionButton sub = *btn;
if (!(btn->state & State_Enabled)) {
sub.palette.setColor(QPalette::ButtonText, t.textDisabledColor);
}
drawControl(CE_PushButtonLabel, &sub, p, w);
}
// ==================== drawPushButtonLabel ====================
void MiniStyle::drawPushButtonLabel(
const QStyleOption *opt, QPainter *p, const QWidget *w
) const {
const auto *btn = qstyleoption_cast<const QStyleOptionButton *>(opt);
if (!btn) return;
auto t = MiniThemeManager::currentTheme();
QRect r = btn->rect;
bool hasText = !btn->text.isEmpty();
bool hasIcon = !btn->icon.isNull();
bool hasMenu = btn->features & QStyleOptionButton::HasMenu;
r.adjust(t.buttonPaddingH, t.buttonPaddingV,
-t.buttonPaddingH, -t.buttonPaddingV);
// menu arrow
QRect arrowR;
if (hasMenu) {
int as = t.arrowSize;
arrowR = QRect(r.right() - as, r.center().y() - as/2, as, as);
r.setRight(arrowR.left() - t.buttonIconSpacing);
}
// icon
QRect iconR;
if (hasIcon) {
QSize is = btn->iconSize.isValid()
? btn->iconSize : QSize(t.iconSize, t.iconSize);
iconR = QRect(r.left(), r.center().y() - is.height()/2,
is.width(), is.height());
r.setLeft(iconR.right() + t.buttonIconSpacing);
}
// text
if (hasText) {
int align = Qt::AlignVCenter
| (hasIcon ? Qt::AlignLeft : Qt::AlignCenter);
p->setPen(btn->palette.color(QPalette::ButtonText));
QString et = p->fontMetrics().elidedText(
btn->text, Qt::ElideRight, r.width());
drawItemText(p, r, align, btn->palette, true, et,
QPalette::ButtonText);
}
// icon draw
if (hasIcon) {
QIcon::Mode m = (btn->state & State_Enabled)
? (btn->state & State_Sunken ? QIcon::Active : QIcon::Normal)
: QIcon::Disabled;
QIcon::State st = (btn->state & State_On) ? QIcon::On : QIcon::Off;
QPixmap pm = btn->icon.pixmap(
btn->iconSize.isValid() ? btn->iconSize
: QSize(t.iconSize, t.iconSize),
m, st);
drawItemPixmap(p, iconR, Qt::AlignCenter, pm);
}
// menu arrow
if (hasMenu) {
QStyleOption ao;
ao.rect = arrowR;
ao.state = btn->state;
ao.palette = btn->palette;
drawPrimitive(PE_IndicatorArrowDown, &ao, p, w);
}
}
6.3 main.cpp — 可运行的测试程序
// ============================================================
// 文件: main.cpp
// ============================================================
#include <QApplication>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QScrollBar>
#include <QTextEdit>
#include <QCheckBox>
#include <QToolButton>
#include <QMenu>
#include <QLabel>
#include "MiniStyle.hpp"
#include "MiniTheme.hpp"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// 设置 Style
app.setStyle(new MiniStyle);
// 加载亮色主题
MiniThemeManager::loadTheme(":/themes/light.json");
// 主窗口
QWidget window;
window.setWindowTitle("MiniStyle Demo — drawPrimitive/drawControl");
auto *layout = new QVBoxLayout(&window);
layout->setSpacing(12);
layout->setContentsMargins(24, 24, 24, 24);
// --- 普通按钮 ---
auto *btn1 = new QPushButton("普通按钮");
layout->addWidget(btn1);
// --- Default 按钮 ---
auto *btn2 = new QPushButton("Default 按钮");
btn2->setDefault(true);
layout->addWidget(btn2);
// --- 图标按钮 ---
auto *btn3 = new QPushButton(
QIcon::fromTheme("document-save"), " 保存");
layout->addWidget(btn3);
// --- 带菜单的按钮 ---
auto *btn4 = new QPushButton("操作");
auto *menu = new QMenu;
menu->addAction("复制");
menu->addAction("粘贴");
menu->addAction("删除");
btn4->setMenu(menu);
layout->addWidget(btn4);
// --- Toggle 按钮 ---
auto *btn5 = new QPushButton("Toggle 按钮");
btn5->setCheckable(true);
layout->addWidget(btn5);
// --- Disabled 按钮 ---
auto *btn6 = new QPushButton("禁用按钮");
btn6->setEnabled(false);
layout->addWidget(btn6);
// --- CheckBox ---
auto *cb = new QCheckBox("我同意用户协议");
layout->addWidget(cb);
// --- 滚动条测试区域 ---
auto *textEdit = new QTextEdit;
textEdit->setPlainText(
"滚动条测试区域\n\n"
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n"
"Sed do eiusmod tempor incididunt ut labore et dolore magna.\n"
"...(more lines)...\n"
);
textEdit->setMaximumHeight(120);
layout->addWidget(textEdit);
// --- 主题切换按钮 ---
auto *toggleBtn = new QPushButton("切换暗色主题");
bool dark = false;
QObject::connect(toggleBtn, &QPushButton::clicked, [&]() {
dark = !dark;
MiniThemeManager::loadTheme(
dark ? ":/themes/dark.json" : ":/themes/light.json");
});
layout->addWidget(toggleBtn);
layout->addStretch();
window.resize(400, 560);
window.show();
return app.exec();
}
6.4 配套 MiniTheme
// ============================================================
// 文件: MiniTheme.hpp
// ============================================================
#pragma once
#include <QColor>
#include <QString>
#include <QJsonObject>
#include <QJsonDocument>
#include <QFile>
struct MiniTheme {
// 按钮
QColor buttonBg { "#F0F0F0" };
QColor buttonHoverBg { "#E0E0E0" };
QColor buttonPressedBg { "#CCCCCC" };
QColor buttonCheckedBg { "#C8DDF8" };
QColor buttonDisabledBg { "#F5F5F5" };
// 边框 & Focus
QColor borderColor { "#C0C0C0" };
QColor borderDisabledColor { "#E0E0E0" };
QColor focusColor { "#4A90D9" };
QColor accentColor { "#2E7BD6" };
QColor textDisabledColor { "#A0A0A0" };
QColor surfaceBg { "#FFFFFF" };
QColor inputBg { "#FFFFFF" };
QColor inputDisabledBg { "#F5F5F5" };
// 滚动条
QColor scrollBarGrooveBg { "#F0F0F0" };
QColor scrollBarSliderBg { "#C0C0C0" };
QColor scrollBarSliderActiveBg { "#A0A0A0" };
QColor scrollBarArrowHoverBg { "#E0E0E0" };
// 几何
qreal borderRadius = 6.0;
qreal borderWidth = 1.5;
qreal focusRingWidth = 2.0;
qreal focusRingMargin = 2.0;
int buttonPaddingH = 16;
int buttonPaddingV = 6;
int buttonIconSpacing = 8;
int buttonMinWidth = 80;
int buttonMinHeight = 30;
int iconSize = 20;
int tabPaddingH = 12;
int tabPaddingV = 6;
int tabMinWidth = 60;
int tabHeight = 32;
int inputPaddingH = 10;
int inputPaddingV = 6;
int inputMinHeight = 30;
int arrowSize = 12;
int checkBoxSize = 18;
int checkBoxSpacing = 6;
int scrollBarWidth = 8;
int scrollBarMinSliderLen = 30;
int scrollBarArrowSize = 16;
qreal scrollBarRadius = 4.0;
int comboBoxPaddingH = 10;
int comboBoxPaddingV = 6;
int sliderHeight = 6;
int sliderWidth = 6;
int sliderHandleSize = 16;
int animationDuration = 200;
};
// 单例管理器
class MiniThemeManager {
public:
static MiniTheme currentTheme() { return s_current; }
static void loadTheme(const QString &path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) return;
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
QJsonObject obj = doc.object();
// 只示例子集,完整版应对所有字段做反序列化
auto color = [&](const QString &key, const QColor &def) -> QColor {
return obj.contains(key)
? QColor(obj[key].toString())
: def;
};
s_current.buttonBg = color("buttonBg", s_current.buttonBg);
s_current.buttonHoverBg = color("buttonHoverBg", s_current.buttonHoverBg);
s_current.buttonPressedBg = color("buttonPressedBg", s_current.buttonPressedBg);
s_current.buttonCheckedBg = color("buttonCheckedBg", s_current.buttonCheckedBg);
s_current.buttonDisabledBg= color("buttonDisabledBg",s_current.buttonDisabledBg);
s_current.borderColor = color("borderColor", s_current.borderColor);
s_current.focusColor = color("focusColor", s_current.focusColor);
s_current.accentColor = color("accentColor", s_current.accentColor);
s_current.surfaceBg = color("surfaceBg", s_current.surfaceBg);
s_current.inputBg = color("inputBg", s_current.inputBg);
s_current.inputDisabledBg = color("inputDisabledBg", s_current.inputDisabledBg);
s_current.textDisabledColor = color("textDisabledColor", s_current.textDisabledColor);
s_current.borderDisabledColor = color("borderDisabledColor", s_current.borderDisabledColor);
s_current.scrollBarGrooveBg = color("scrollBarGrooveBg", s_current.scrollBarGrooveBg);
s_current.scrollBarSliderBg = color("scrollBarSliderBg", s_current.scrollBarSliderBg);
s_current.scrollBarSliderActiveBg = color("scrollBarSliderActiveBg", s_current.scrollBarSliderActiveBg);
s_current.scrollBarArrowHoverBg = color("scrollBarArrowHoverBg", s_current.scrollBarArrowHoverBg);
s_current.borderRadius = obj.value("borderRadius").toDouble(s_current.borderRadius);
s_current.borderWidth = obj.value("borderWidth").toDouble(s_current.borderWidth);
s_current.focusRingWidth = obj.value("focusRingWidth").toDouble(s_current.focusRingWidth);
s_current.focusRingMargin = obj.value("focusRingMargin").toDouble(s_current.focusRingMargin);
s_current.buttonMinWidth = obj.value("buttonMinWidth").toInt(s_current.buttonMinWidth);
s_current.buttonMinHeight = obj.value("buttonMinHeight").toInt(s_current.buttonMinHeight);
s_current.buttonPaddingH = obj.value("buttonPaddingH").toInt(s_current.buttonPaddingH);
s_current.buttonPaddingV = obj.value("buttonPaddingV").toInt(s_current.buttonPaddingV);
s_current.iconSize = obj.value("iconSize").toInt(s_current.iconSize);
s_current.arrowSize = obj.value("arrowSize").toInt(s_current.arrowSize);
s_current.checkBoxSize = obj.value("checkBoxSize").toInt(s_current.checkBoxSize);
s_current.checkBoxSpacing = obj.value("checkBoxSpacing").toInt(s_current.checkBoxSpacing);
s_current.scrollBarWidth = obj.value("scrollBarWidth").toInt(s_current.scrollBarWidth);
}
private:
static inline MiniTheme s_current;
};
7. 参考源文件对照
本课内容对应 qlementine 工程中的以下文件:
| 本课内容 | qlementine 对应文件 |
|---|---|
| drawPrimitive (PE_FrameButtonBevel) | src/QlementineStyle.cpp — drawPrimitive() |
| drawControl (CE_PushButton) | src/QlementineStyle.cpp — drawControl() |
| drawComplexControl (CC_ScrollBar) | src/QlementineStyle.cpp — drawComplexControl() |
| subControlRect | src/QlementineStyle.cpp — subControlRect() |
| 绘制原语工具箱 | src/PrimitiveUtils.hpp / src/PrimitiveUtils.cpp |
| sizeFromContents | src/QlementineStyle.cpp — sizeFromContents() |
| pixelMetric | src/QlementineStyle.cpp — pixelMetric() |
| styleHint | src/QlementineStyle.cpp — styleHint() |
qlementine 的 PrimitiveUtils 比本文的工具箱丰富很多,包含了阴影绘制、渐变填充、SVG 图标缓存等高级功能。建议本课学完后直接阅读其源码。
课后任务
- 补全 drawPrimitive:实现
PE_IndicatorCheckBox(矩形勾号 + 半选态方块)和PE_IndicatorRadioButton(外圆空心 + 内圆实心) - 实现 CE_ProgressBar:画一个圆角进度条,背景灰色,进度用 accent 色
- 扩展 Theme:给 MiniTheme 加上
progressBarBg、progressBarFill、progressBarTextColor - 添加动画:给按钮 hover/press 加 200ms 的背景色渐变(留给第5课的内容,但可以先想想怎么做)
下一课:动画系统 — WidgetAnimationManager、焦点动画、Switch 控件实战