前三课我们搭好了骨架: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_MouseOverState_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

drawControldrawPrimitive 高一层。它负责整个"逻辑控件"的绘制,内部会调用多次 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_PushButtonBevelPE_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);
}

pixelMetricsizeFromContents 之间的分工:

  • 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.cppdrawPrimitive()
drawControl (CE_PushButton) src/QlementineStyle.cppdrawControl()
drawComplexControl (CC_ScrollBar) src/QlementineStyle.cppdrawComplexControl()
subControlRect src/QlementineStyle.cppsubControlRect()
绘制原语工具箱 src/PrimitiveUtils.hpp / src/PrimitiveUtils.cpp
sizeFromContents src/QlementineStyle.cppsizeFromContents()
pixelMetric src/QlementineStyle.cpppixelMetric()
styleHint src/QlementineStyle.cppstyleHint()

qlementine 的 PrimitiveUtils 比本文的工具箱丰富很多,包含了阴影绘制、渐变填充、SVG 图标缓存等高级功能。建议本课学完后直接阅读其源码。

课后任务

  1. 补全 drawPrimitive:实现 PE_IndicatorCheckBox(矩形勾号 + 半选态方块)和 PE_IndicatorRadioButton(外圆空心 + 内圆实心)
  2. 实现 CE_ProgressBar:画一个圆角进度条,背景灰色,进度用 accent 色
  3. 扩展 Theme:给 MiniTheme 加上 progressBarBgprogressBarFillprogressBarTextColor
  4. 添加动画:给按钮 hover/press 加 200ms 的背景色渐变(留给第5课的内容,但可以先想想怎么做)

下一课动画系统 — WidgetAnimationManager、焦点动画、Switch 控件实战