引言

前五篇文章从 QStyle 基础到动画系统,逐步构建了一个可用的自定义 UI 框架。但实用的框架还需要三块拼图:扩展控件绘制能力现代化的图标系统、以及 Qt5/Qt6 跨版本兼容。本文把这些内容一次性讲清楚。

读完你将能:

  • 在不破坏 Qt 原生枚举的前提下扩展 QStyle 的绘制元素
  • 实现一个支持自动着色、像素级缓存的 SVG 图标系统
  • 用 CMake 同时支持 Qt5 和 Qt6 编译
  • 写出零运行时开销的 Qt 版本兼容层

1. 扩展 QStyle:自定义枚举与虚函数

QStyle 的绘制能力通过 PrimitiveElementControlElementPixelMetric 等枚举来索引。Qt 自身预留了 CustomBase 作为用户扩展起点,但最稳妥的方式是完全不碰 Qt 枚举空间,用独立枚举 + 独立虚函数接口。

1.1 定义扩展枚举

// StyleExtension.hpp
#pragma once

namespace MiniStyle {

// 扩展的绘制元素 — 完全独立于 QStyle::PrimitiveElement
enum class ExtendedPrimitive {
    CommandButtonPanel = 0,       // 复合命令按钮面板
    CommandButtonSplitter,        // 分隔线
    StatusIndicatorDot,           // 状态指示点
    Count
};

// 扩展的控件元素
enum class ExtendedControl {
    CommandButton = 0,            // 完整的命令按钮
    CollapsibleSection,           // 可折叠区域
    Count
};

// 扩展的像素度量
enum class ExtendedPixelMetric {
    CommandButtonIconSize = 0,
    CommandButtonSpacing,
    CommandButtonMinWidth,
    Count
};

} // namespace MiniStyle

为什么不用 Qt 的 CustomBase

  1. 隔离性:自定义枚举值与 Qt 版本解耦,升级 Qt 不会引入冲突。
  2. 类型安全enum class 无法隐式转换,编译器帮你检查传参错误。
  3. 清晰边界:一眼就知道哪些是"我们画的”,哪些是 Qt 原生的。

1.2 在 QStyle 子类中添加扩展接口

// MiniStyle.hpp
#pragma once
#include <QProxyStyle>
#include "StyleExtension.hpp"

class MiniStyle : public QProxyStyle {
    Q_OBJECT
public:
    // --- 标准 Qt 绘制入口(覆盖) ---
    void drawPrimitive(PrimitiveElement elem, const QStyleOption *opt,
                       QPainter *p, const QWidget *w = nullptr) const override;
    void drawControl(ControlElement elem, const QStyleOption *opt,
                     QPainter *p, const QWidget *w = nullptr) const override;
    int pixelMetric(PixelMetric metric, const QStyleOption *opt = nullptr,
                    const QWidget *w = nullptr) const override;

    // --- 扩展绘制入口(纯虚,由具体风格实现) ---
    virtual void drawExtendedPrimitive(ExtendedPrimitive elem,
        const QStyleOption *opt, QPainter *p, const QWidget *w = nullptr) const;

    virtual void drawExtendedControl(ExtendedControl elem,
        const QStyleOption *opt, QPainter *p, const QWidget *w = nullptr) const;

    virtual int extendedPixelMetric(ExtendedPixelMetric metric,
        const QStyleOption *opt = nullptr, const QWidget *w = nullptr) const;

protected:
    // 子类重写这些来实现具体绘制
    virtual void drawCommandButtonPanel(const QStyleOption *opt,
        QPainter *p, const QWidget *w) const;
    // ...
};

1.3 实现分发逻辑

// MiniStyle.cpp
void MiniStyle::drawExtendedPrimitive(ExtendedPrimitive elem,
    const QStyleOption *opt, QPainter *p, const QWidget *w) const
{
    switch (elem) {
    case ExtendedPrimitive::CommandButtonPanel:
        drawCommandButtonPanel(opt, p, w);
        break;
    case ExtendedPrimitive::StatusIndicatorDot:
        drawStatusIndicatorDot(opt, p, w);
        break;
    default:
        break;
    }
}

int MiniStyle::extendedPixelMetric(ExtendedPixelMetric metric,
    const QStyleOption *opt, const QWidget *w) const
{
    switch (metric) {
    case ExtendedPixelMetric::CommandButtonIconSize: return 32;
    case ExtendedPixelMetric::CommandButtonSpacing:  return 8;
    case ExtendedPixelMetric::CommandButtonMinWidth:  return 200;
    default: return -1;
    }
}

1.4 实战:CommandButton 复合控件

CommandButton 是一个带标题和描述的大号按钮,类似 VSCode 的命令面板条目:

┌──────────────────────────────────┐
│  [图标]  New File                │
│          Create a new empty file │
└──────────────────────────────────┘

对应的 StyleOption:

// CommandButton.hpp
#pragma once
#include <QStyleOption>
#include <QIcon>

struct CommandButtonOption : public QStyleOption {
    static constexpr int Type = QStyleOption::SO_CustomBase + 100;
    int type() const override { return Type; }

    QString title;
    QString description;
    QIcon   icon;
    bool    isHovered  = false;
    bool    isSelected = false;
};

// 便捷构造
inline CommandButtonOption initCommandButtonOption(const QWidget *w) {
    CommandButtonOption opt;
    opt.initFrom(w);
    return opt;
}

绘制实现:

void MiniStyle::drawCommandButtonPanel(const QStyleOption *baseOpt,
    QPainter *p, const QWidget *w) const
{
    const auto *opt = qstyleoption_cast<const CommandButtonOption*>(baseOpt);
    if (!opt) return;

    QRect rect = opt->rect;
    p->save();
    p->setRenderHint(QPainter::Antialiasing);

    // 背景
    QColor bg = opt->isHovered ? QColor(0x40, 0x40, 0x40)
                               : QColor(0x30, 0x30, 0x30);
    if (opt->isSelected)
        bg = QColor(0x50, 0x50, 0x60);

    p->setBrush(bg);
    p->setPen(Qt::NoPen);
    p->drawRoundedRect(rect, 4, 4);

    // 图标区域
    const int iconSize = 24;
    const int margin   = 10;
    QRect iconRect(rect.left() + margin,
                   rect.center().y() - iconSize / 2,
                   iconSize, iconSize);

    if (!opt->icon.isNull()) {
        QPixmap pm = opt->icon.pixmap(iconSize, iconSize);
        p->drawPixmap(iconRect, pm);
    }

    // 标题
    QRect textRect(iconRect.right() + 10, rect.top() + 8,
                   rect.width() - iconRect.width() - margin * 2 - 10,
                   rect.height() / 2);

    QFont titleFont = p->font();
    titleFont.setPointSize(11);
    titleFont.setBold(true);
    p->setFont(titleFont);
    p->setPen(Qt::white);
    p->drawText(textRect, Qt::AlignLeft | Qt::AlignBottom, opt->title);

    // 描述
    QRect descRect(iconRect.right() + 10, rect.center().y() + 2,
                   textRect.width(), rect.height() / 2 - 4);

    QFont descFont = p->font();
    descFont.setPointSize(9);
    p->setFont(descFont);
    p->setPen(QColor(0xAA, 0xAA, 0xAA));
    p->drawText(descRect, Qt::AlignLeft | Qt::AlignTop, opt->description);

    p->restore();
}

使用方式:

// 在自定义 widget 的 paintEvent 中
void CommandButtonWidget::paintEvent(QPaintEvent *) {
    QPainter p(this);
    CommandButtonOption opt = initCommandButtonOption(this);
    opt.title       = "Open File";
    opt.description = "Browse and open a file from disk";
    opt.icon        = QIcon(":/icons/file-open.svg");
    opt.isHovered   = m_hovered;

    // 直接调用 MiniStyle 的扩展接口
    auto *style = qobject_cast<MiniStyle*>(this->style());
    if (style) {
        style->drawExtendedPrimitive(
            ExtendedPrimitive::CommandButtonPanel, &opt, &p, this);
    }
}

2. SVG 图标系统

SVG 是矢量格式,适合任意缩放的界面。但直接用 QSvgRenderer 每次实时渲染性能堪忧——我们需要一套缓存 + 自动着色的方案。

2.1 核心渲染:SVG → QPixmap

// IconEngine.hpp
#pragma once
#include <QPixmap>
#include <QSvgRenderer>
#include <QPixmapCache>
#include <QFile>

namespace MiniStyle {

// 加载 SVG 并渲染到指定尺寸的 QPixmap
QPixmap loadSvg(const QString &path, QSize size) {
    if (path.isEmpty() || !QFile::exists(path))
        return {};

    QSvgRenderer renderer(path);
    if (!renderer.isValid())
        return {};

    QPixmap pixmap(size * renderer.devicePixelRatioF());
    pixmap.setDevicePixelRatio(renderer.devicePixelRatioF());
    pixmap.fill(Qt::transparent);

    QPainter p(&pixmap);
    p.setRenderHint(QPainter::Antialiasing);
    renderer.render(&p);
    p.end();

    return pixmap;
}

} // namespace MiniStyle

2.2 自动图标着色:AutoIconColor

核心思想:用 QWidget::palette() 的文本色自动为单色图标上色。

// IconEngine.hpp(续)

// 对已有 pixmap 进行颜色染色(保留 alpha 通道)
QPixmap colorizePixmap(const QPixmap &source, const QColor &color) {
    if (source.isNull()) return {};

    QImage img = source.toImage();
    img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);

    const int r = color.red();
    const int g = color.green();
    const int b = color.blue();

    for (int y = 0; y < img.height(); ++y) {
        QRgb *line = reinterpret_cast<QRgb*>(img.scanLine(y));
        for (int x = 0; x < img.width(); ++x) {
            int alpha = qAlpha(line[x]);
            if (alpha > 0) {
                line[x] = qPremultiply(qRgba(r, g, b, alpha));
            }
        }
    }

    return QPixmap::fromImage(img);
}

// 获取适合指定 widget 的文本颜色
QColor autoIconColor(const QWidget *widget) {
    if (!widget) return Qt::black;
    return widget->palette().color(QPalette::Normal, QPalette::WindowText);
}

2.3 QPixmapCache 缓存策略

缓存键需要包含所有影响最终图片的因素:

// IconEngine.hpp(续)

QString iconCacheKey(const QString &path, QSize size,
                     QIcon::Mode mode, QIcon::State state) {
    return QStringLiteral("mini:%1:%2x%3:%4:%5")
        .arg(path)
        .arg(size.width()).arg(size.height())
        .arg(static_cast<int>(mode))
        .arg(static_cast<int>(state));
}

QPixmap cachedSvgIcon(const QString &path, QSize size,
                      QIcon::Mode mode = QIcon::Normal,
                      QIcon::State state = QIcon::Off,
                      const QColor &tint = QColor()) {
    // 1. 先查缓存(不含颜色 — 颜色由调用方控制)
    const QString key = iconCacheKey(path, size, mode, state);

    QPixmap cached;
    if (QPixmapCache::find(key, &cached))
        return cached;

    // 2. 渲染
    QPixmap pixmap = loadSvg(path, size);
    if (pixmap.isNull())
        return {};

    // 3. 根据 mode/state 调整
    if (mode == QIcon::Disabled) {
        // 变灰
        QImage img = pixmap.toImage();
        for (int y = 0; y < img.height(); ++y) {
            QRgb *line = reinterpret_cast<QRgb*>(img.scanLine(y));
            for (int x = 0; x < img.width(); ++x) {
                int gray = qGray(line[x]);
                int alpha = qAlpha(line[x]);
                line[x] = qPremultiply(qRgba(gray, gray, gray,
                    static_cast<int>(alpha * 0.5)));
            }
        }
        pixmap = QPixmap::fromImage(img);
    }

    // 4. 存入缓存
    QPixmapCache::insert(key, pixmap);
    return pixmap;
}

2.4 主题感知图标:makeThemedIcon

将整个流程封装为工厂函数,配合主题系统:

// IconEngine.hpp(续)

struct ThemedIcon {
    QString  darkPath;
    QString  lightPath;
    QColor   tint;        // 留空则用自动颜色
    QSize    defaultSize = QSize(16, 16);
};

QPixmap makeThemedIcon(const ThemedIcon &icon,
                       const QWidget *context,
                       QSize size = QSize(),
                       QIcon::Mode mode = QIcon::Normal) {
    if (!size.isValid())
        size = icon.defaultSize;

    // 根据主题选择路径
    QString path = icon.lightPath; // 默认亮色
    if (m_currentTheme == Theme::Dark && !icon.darkPath.isEmpty())
        path = icon.darkPath;

    return cachedSvgIcon(path, size, mode, QIcon::Off, icon.tint);
}

完整使用示例:

// 定义图标集
namespace AppIcons {
    const ThemedIcon newFile = {
        .lightPath = ":/icons/light/new-file.svg",
        .darkPath  = ":/icons/dark/new-file.svg",
        .defaultSize = QSize(16, 16)
    };
    const ThemedIcon save = {
        .lightPath = ":/icons/light/save.svg",
        .darkPath  = ":/icons/dark/save.svg",
    };
}

// 在 paintEvent 中使用
void MyToolbar::paintEvent(QPaintEvent *e) {
    QPainter p(this);
    QPixmap icon = makeThemedIcon(AppIcons::newFile, this, QSize(16, 16));
    p.drawPixmap(5, 5, icon);
}

3. 应用集成

3.1 启动入口

// main.cpp
#include <QApplication>
#include "MiniStyle.hpp"
#include "ThemeManager.hpp"

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    // 1. 设置自定义风格
    app.setStyle(new MiniStyle());

    // 2. 加载主题
    auto &theme = ThemeManager::instance();
    theme.setThemeJsonPath(":/themes/dark.json");

    // 3. 主题变更 → 强制全量重绘
    QObject::connect(&theme, &ThemeManager::themeChanged, [&app]() {
        // 方法 A:通知所有顶层窗口
        for (QWidget *w : app.topLevelWidgets()) {
            w->update();                          // 触发 repaint
            // 递归刷新子控件
            triggerUpdateRecursive(w);
        }

        // 方法 B(更激进):重新设置调色板
        // app.setPalette(theme.currentPalette());
        // qApp->style()->polish(qApp);  // 广播 polish 事件
    });

    MainWindow w;
    w.show();
    return app.exec();
}

3.2 递归刷新辅助函数

static void triggerUpdateRecursive(QWidget *widget) {
    widget->update();
    for (QObject *child : widget->children()) {
        if (auto *w = qobject_cast<QWidget*>(child)) {
            triggerUpdateRecursive(w);
        }
    }
}

3.3 信号接线最佳实践

// ThemeManager.hpp 片段
class ThemeManager : public QObject {
    Q_OBJECT
public:
    static ThemeManager &instance();

    void setThemeJsonPath(const QString &path);
    const ThemeDefinition &currentTheme() const;

signals:
    void themeChanged();  // 一切主题变更的统一信号
};

关键原则:

  • 单一信号:一切主题变更(颜色、字体、间距)都触发同一个 themeChanged()
  • 全量刷新:不要试图做增量更新——复杂度不值得。全量 update() 在现代硬件上几乎无感。
  • polish 重入:调用 QStyle::polish() 会重新发送 Polish 事件给所有 widget,这是 Qt 原生的主题切换通知机制。

4. Qt5 / Qt6 双版本兼容

这是大多数库作者的头号痛点。Qt6 引入了大量"优化性破坏”(enum class、API 签名变更等)。本节提供一套完整的兼容方案。

4.1 策略:编译期 #if 分支

核心原则:零运行时开销。所有版本分支在预处理阶段解决,不引入虚表、条件判断或动态判断。

4.2 QtCompat.hpp — 完整兼容头

// QtCompat.hpp
#pragma once
#include <QtCore/qglobal.h>

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#  include <QtWidgets/QStyleOptionTab>
#else
#  include <QtWidgets/QStyleOptionTab>
#endif
#include <QVariant>
#include <QEvent>
#include <QRect>
#include <QRectF>

// ============================================================
// 1. 枚举作用域兼容
// ============================================================
//
// Qt5 使用扁平枚举:QStyle::PE_FrameFocusRect
// Qt6 使用 scoped enum:QStyle::PrimitiveElement::PE_FrameFocusRect
//
// 我们为常用枚举值定义宏,让代码"看起来"像 Qt6 的写法
// 但在 Qt5 下也能编译通过。

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    // Qt6: scoped enums — 直接用
    // 无需宏
#else
    // Qt5: 手动定义兼容别名
    namespace MiniStyleCompat {
        using PrimitiveElement = QStyle::PrimitiveElement;
        constexpr auto PE_FrameFocusRect = QStyle::PE_FrameFocusRect;
        constexpr auto PE_Frame          = QStyle::PE_Frame;
        constexpr auto PE_FrameLineEdit  = QStyle::PE_FrameLineEdit;
        constexpr auto PE_PanelButtonBevel   = QStyle::PE_PanelButtonBevel;
        constexpr auto PE_IndicatorArrowDown = QStyle::PE_IndicatorArrowDown;
        constexpr auto PE_IndicatorArrowUp   = QStyle::PE_IndicatorArrowUp;
        constexpr auto PE_IndicatorArrowLeft = QStyle::PE_IndicatorArrowLeft;
        constexpr auto PE_IndicatorArrowRight= QStyle::PE_IndicatorArrowRight;
        constexpr auto PE_PanelItemViewItem  = QStyle::PE_PanelItemViewItem;
        constexpr auto PE_IndicatorItemViewItemDrop = QStyle::PE_IndicatorItemViewItemDrop;
        constexpr auto PE_IndicatorBranch     = QStyle::PE_IndicatorBranch;
        constexpr auto PE_IndicatorCheckBox   = QStyle::PE_IndicatorCheckBox;
        constexpr auto PE_IndicatorRadioButton = QStyle::PE_IndicatorRadioButton;
        constexpr auto PE_Widget              = QStyle::PE_Widget;
        constexpr auto PE_PanelMenu           = QStyle::PE_PanelMenu;
        constexpr auto PE_PanelScrollAreaCorner = QStyle::PE_PanelScrollAreaCorner;

        using ControlElement = QStyle::ControlElement;
        constexpr auto CE_PushButton    = QStyle::CE_PushButton;
        constexpr auto CE_PushButtonBevel = QStyle::CE_PushButtonBevel;
        constexpr auto CE_PushButtonLabel = QStyle::CE_PushButtonLabel;
        constexpr auto CE_CheckBox       = QStyle::CE_CheckBox;
        constexpr auto CE_CheckBoxLabel  = QStyle::CE_CheckBoxLabel;
        constexpr auto CE_RadioButton     = QStyle::CE_RadioButton;
        constexpr auto CE_RadioButtonLabel= QStyle::CE_RadioButtonLabel;
        constexpr auto CE_TabBarTab       = QStyle::CE_TabBarTab;
        constexpr auto CE_TabBarTabLabel  = QStyle::CE_TabBarTabLabel;
        constexpr auto CE_TabBarTabShape  = QStyle::CE_TabBarTabShape;
        constexpr auto CE_ProgressBar     = QStyle::CE_ProgressBar;
        constexpr auto CE_ProgressBarContents = QStyle::CE_ProgressBarContents;
        constexpr auto CE_ProgressBarGroove   = QStyle::CE_ProgressBarGroove;
        constexpr auto CE_ProgressBarLabel    = QStyle::CE_ProgressBarLabel;
        constexpr auto CE_MenuItem         = QStyle::CE_MenuItem;
        constexpr auto CE_MenuScroller     = QStyle::CE_MenuScroller;
        constexpr auto CE_MenuBarItem      = QStyle::CE_MenuBarItem;
        constexpr auto CE_MenuBarEmptyArea = QStyle::CE_MenuBarEmptyArea;
        constexpr auto CE_ScrollBarAddLine  = QStyle::CE_ScrollBarAddLine;
        constexpr auto CE_ScrollBarSubLine  = QStyle::CE_ScrollBarSubLine;
        constexpr auto CE_ScrollBarAddPage  = QStyle::CE_ScrollBarAddPage;
        constexpr auto CE_ScrollBarSubPage  = QStyle::CE_ScrollBarSubPage;
        constexpr auto CE_ScrollBarSlider   = QStyle::CE_ScrollBarSlider;
        constexpr auto CE_ScrollBarFirst    = QStyle::CE_ScrollBarFirst;
        constexpr auto CE_ScrollBarLast     = QStyle::CE_ScrollBarLast;
        constexpr auto CE_ComboBoxLabel     = QStyle::CE_ComboBoxLabel;
        constexpr auto CE_ToolButtonLabel   = QStyle::CE_ToolButtonLabel;
        constexpr auto CE_HeaderLabel       = QStyle::CE_HeaderLabel;
        constexpr auto CE_HeaderSection     = QStyle::CE_HeaderSection;
        constexpr auto CE_SizeGrip          = QStyle::CE_SizeGrip;

        using PixelMetric = QStyle::PixelMetric;
        constexpr auto PM_DefaultFrameWidth  = QStyle::PM_DefaultFrameWidth;
        constexpr auto PM_ButtonMargin       = QStyle::PM_ButtonMargin;
        constexpr auto PM_ScrollBarExtent    = QStyle::PM_ScrollBarExtent;
        constexpr auto PM_ScrollBarSliderMin = QStyle::PM_ScrollBarSliderMin;
    }
#endif

// ============================================================
// 2. QIcon::pixmap() 参数差异
// ============================================================
//
// Qt6: pixmap(size, devicePixelRatio)
// Qt5: pixmap(size, Mode, State)   — 没有 DPR 参数

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    #define MINI_ICON_PIXMAP_EX(icon, size, dpr, mode, state) \
        (icon).pixmap(size, dpr, (mode), (state))
    #define MINI_ICON_PIXMAP(icon, size) \
        (icon).pixmap(size)
#else
    #define MINI_ICON_PIXMAP_EX(icon, size, dpr, mode, state) \
        (icon).pixmap(size, (mode), (state))
    #define MINI_ICON_PIXMAP(icon, size) \
        (icon).pixmap(size)
#endif

// 不使用宏的写法(推荐用于 C++ 代码中):
inline QPixmap compatIconPixmap(const QIcon &icon, const QSize &size,
                                 qreal dpr = 1.0,
                                 QIcon::Mode mode = QIcon::Normal,
                                 QIcon::State state = QIcon::Off) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    QSize pixelSize = size * dpr;
    QPixmap pm = icon.pixmap(pixelSize, dpr);
    pm.setDevicePixelRatio(dpr);
    return pm;
#else
    Q_UNUSED(dpr);
    return icon.pixmap(size, mode, state);
#endif
}

// ============================================================
// 3. QVariant::type() / typeId() 差异
// ============================================================
//
// Qt6: QVariant::typeId()  返回 int
// Qt5: QVariant::type()    返回 Type (int)
//
// 两者的 metaType().id() 通用

template<typename T>
inline int compatVariantTypeId(const QVariant &v) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    return v.metaType().id();
#else
    return static_cast<int>(v.type());
#endif
}

// 也可以直接用这个(Qt5/Qt6 通用):
// QMetaType::fromType<T>().id()   — Qt6
// qMetaTypeId<T>()                — Qt5/Qt6 通用(废弃但在 Qt6 中可用)

// ============================================================
// 4. QEnterEvent vs QEvent
// ============================================================
//
// Qt6: enterEvent(QEnterEvent *event)   // QEnterEvent 是独立类型
// Qt5: enterEvent(QEvent *event)        // 通用 QEvent

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    using EnterEventType = QEnterEvent;
    #define MINI_ENTER_EVENT_CAST(event) \
        static_cast<QEnterEvent*>(event)
    #define MINI_ENTER_EVENT_POS(event) \
        (event)->position().toPoint()
#else
    using EnterEventType = QEvent;
    #define MINI_ENTER_EVENT_CAST(event) \
        static_cast<QEvent*>(event)
    #define MINI_ENTER_EVENT_POS(event) \
        QCursor::pos()  // Qt5 没有 event->pos() for QEvent
#endif

// 推荐的内联函数写法:
inline QPoint compatEnterPos(
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    QEnterEvent *event
#else
    QEvent *event
#endif
) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    return event->position().toPoint();
#else
    Q_UNUSED(event);
    return QCursor::pos();
#endif
}

// ============================================================
// 5. QStyleOptionTab::tabIndex — 基类差异
// ============================================================
//
// Qt6: QStyleOption 包含 tabIndex(移到基类)
// Qt5: 需要 QStyleOptionTabV4(V4 版本才有 tabIndex)

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    using CompatTabOption = QStyleOptionTab;
#else
    using CompatTabOption = QStyleOptionTabV4;
#endif

inline int compatTabIndex(const QStyleOptionTab *opt) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    return opt->tabIndex;
#else
    // Qt5: 如果是 V4 版本,tabIndex 可用;否则返回 0
    if (opt->version >= 4)
        return opt->tabIndex;
    return 0;
#endif
}

// ============================================================
// 6. rect.toRectF() — Qt6 新增
// ============================================================
//
// Qt6: QRect 有 toRectF() 成员函数
// Qt5: 需要显式构造 QRectF

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    #define MINI_TO_RECTF(rect) (rect).toRectF()
#else
    #define MINI_TO_RECTF(rect) QRectF(rect)
#endif

// 推荐直接用 QRectF(rect) — 两种版本通用
// QRectF::QRectF(const QRect &) 在 Qt5 和 Qt6 中都存在

// ============================================================
// 7. QMenu::addAction() 参数顺序差异
// ============================================================
//
// Qt6: addAction(text, shortcut, receiver, slot)
// Qt5: addAction(text, receiver, slot, shortcut)

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    #define MINI_MENU_ADD_ACTION(menu, text, shortcut, receiver, slot) \
        (menu)->addAction(text, shortcut, (receiver), (slot))
#else
    #define MINI_MENU_ADD_ACTION(menu, text, shortcut, receiver, slot) \
        (menu)->addAction(text, (receiver), (slot), shortcut)
#endif

// 推荐的免宏写法(跳过 addAction 直接构建 QAction):
template<typename Func>
inline QAction *compatAddAction(QMenu *menu, const QString &text,
                                 const QKeySequence &shortcut,
                                 const QObject *receiver, Func slot) {
    QAction *action = new QAction(text, menu);
    action->setShortcut(shortcut);
    QObject::connect(action, &QAction::triggered, receiver, slot);
    menu->addAction(action);
    return action;
}

// ============================================================
// 8. 保持宽高比枚举
// ============================================================
//
// Qt6: Qt::KeepAspectRatio         (enum class)
// Qt5: Qt::KeepAspectRatio         (flat enum)

// Qt6 scoped enum 不能隐式转换,但 Qt::KeepAspectRatio 实际上
// 在两种版本中都可以直接使用,因为 Qt6 允许 named enum 的隐式转换
// 到 int(尽管是 scoped enum)。
// 所以大多数情况下不需要宏。

// 但有冲突的少数情况需要统一
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    // Qt6: 是 enum class Qt::AspectRatioMode
    #define MINI_KEEP_ASPECT_RATIO   Qt::KeepAspectRatio
    #define MINI_IGNORE_ASPECT_RATIO Qt::IgnoreAspectRatio
#else
    #define MINI_KEEP_ASPECT_RATIO   Qt::KeepAspectRatio
    #define MINI_IGNORE_ASPECT_RATIO Qt::IgnoreAspectRatio
#endif

// ============================================================
// 9. QPainterPath 元素类型
// ============================================================
//
// Qt6: QPainterPath::ElementType 是 enum class
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    namespace MiniStyleCompat {
        using ElementType = QPainterPath::ElementType;
        constexpr auto MoveToElement  = QPainterPath::MoveToElement;
        constexpr auto LineToElement  = QPainterPath::LineToElement;
        constexpr auto CurveToElement = QPainterPath::CurveToElement;
    }
#endif

// ============================================================
// 10. 字符串视图(QStringView 跨版本约束)
// ============================================================
//
// Qt6: QStringView 完全可用
// Qt5: 仅在 C++17 下可用(Qt 5.12+)
// 安全写法:直接用 const QString& 传递

// ============================================================
// 11. 方便的函数转换
// ============================================================

// QRegularExpression 兼容(Qt6 中 QRegExp 已完全移除)
#include <QRegularExpression>

// 如果在 Qt5 代码中使用了 QRegExp,用这个迁移:
inline QRegularExpression compatRegex(const QString &pattern) {
    return QRegularExpression(pattern);
}

#endif // QtCompat_hpp

4.3 CMakeLists.txt 双版本配置

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)

# ---- 检测 Qt 版本 ----
# 用户通过 -DQT_VERSION=5 或 -DQT_VERSION=6 指定
set(QT_VERSION "6" CACHE STRING "Qt major version (5 or 6)")

if(QT_VERSION STREQUAL "5")
    find_package(Qt5 COMPONENTS Widgets Svg REQUIRED)
    set(QT_LIBS Qt5::Widgets Qt5::Svg)
    set(QT_VERSION_MAJOR 5)

    # Qt5 用传统的 add_library
    add_library(miniStyle STATIC
        MiniStyle.cpp
        MiniStyle.hpp
        MiniStylePlugin.cpp
        ThemeManager.cpp
        IconEngine.cpp
        QtCompat.hpp
        StyleExtension.hpp
    )
    target_include_directories(miniStyle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
    target_link_libraries(miniStyle PUBLIC Qt5::Widgets Qt5::Svg)
    target_compile_definitions(miniStyle PRIVATE MINI_STYLE_LIBRARY)

else()
    find_package(Qt6 COMPONENTS Widgets WidgetsTools Svg SvgWidgets REQUIRED)
    set(QT_LIBS Qt6::Widgets Qt6::Svg Qt6::SvgWidgets)
    set(QT_VERSION_MAJOR 6)

    # Qt6 推荐用 qt_add_library(自动处理 MOC/UIC 等)
    qt_add_library(miniStyle STATIC
        MiniStyle.cpp
        MiniStyle.hpp
        MiniStylePlugin.cpp
        ThemeManager.cpp
        IconEngine.cpp
        QtCompat.hpp
        StyleExtension.hpp
    )
    target_include_directories(miniStyle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
    target_link_libraries(miniStyle PUBLIC Qt6::Widgets Qt6::Svg Qt6::SvgWidgets)
    target_compile_definitions(miniStyle PRIVATE MINI_STYLE_LIBRARY)
endif()

# ---- 公共设置 ----
set_target_properties(miniStyle PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    AUTOMOC ON
    AUTOUIC ON
    AUTORCC ON
)

4.4 编译脚本

#!/bin/bash
# build.sh — 同时测试 Qt5 和 Qt6 构建

echo "=== Building with Qt6 ==="
mkdir -p build-qt6 && cd build-qt6
cmake .. -DQT_VERSION=6 -DCMAKE_PREFIX_PATH=/opt/Qt/6.5.0/gcc_64
cmake --build . -j$(nproc)

echo ""
echo "=== Building with Qt5 ==="
cd .. && mkdir -p build-qt5 && cd build-qt5
cmake .. -DQT_VERSION=5 -DCMAKE_PREFIX_PATH=/opt/Qt/5.15.2/gcc_64
cmake --build . -j$(nproc)

echo "Both builds completed."

5. 架构总览:MiniStyle 完整文件结构

到本文为止,六篇文章覆盖了自定义 QStyle 框架的全部核心代码。下面展示最终的文件结构和 CMake 配置。

5.1 目录结构

mini-style/
├── CMakeLists.txt                  # 主构建文件(双版本 Qt)
├── README.md
├── assets/
│   └── themes/
│       ├── dark.json               # 暗色主题定义
│       └── light.json              # 亮色主题定义
├── resources/
│   ├── icons/
│   │   ├── dark/                   # 暗色主题 SVG 图标
│   │   │   ├── new-file.svg
│   │   │   ├── open.svg
│   │   │   ├── save.svg
│   │   │   └── close.svg
│   │   └── light/                  # 亮色主题 SVG 图标
│   │       ├── new-file.svg
│   │       ├── open.svg
│   │       ├── save.svg
│   │       └── close.svg
│   └── resources.qrc               # Qt 资源文件
├── src/
│   ├── MiniStyle.hpp               # 主风格类声明
│   ├── MiniStyle.cpp               # 主风格绘制实现
│   ├── MiniStylePlugin.cpp         # Qt Designer 插件
│   ├── ThemeManager.hpp            # 主题管理器
│   ├── ThemeManager.cpp            # 主题管理实现(JSON 解析)
│   ├── IconEngine.hpp              # SVG 图标引擎
│   ├── IconEngine.cpp              # 图标引擎实现
│   ├── AnimationEngine.hpp         # 动画引擎
│   ├── AnimationEngine.cpp         # 动画引擎实现
│   ├── QtCompat.hpp                # Qt5/Qt6 兼容层
│   ├── StyleExtension.hpp          # 扩展枚举定义
│   └── widgets/
│       ├── CommandButton.hpp       # 复合命令按钮 widget
│       └── CollapsibleSection.hpp  # 可折叠区域 widget
├── examples/
│   ├── demo/
│   │   ├── CMakeLists.txt
│   │   └── main.cpp                # 完整演示应用
│   └── minimal/
│       ├── CMakeLists.txt
│       └── main.cpp                # 最小可运行示例
└── tests/
    ├── CMakeLists.txt
    ├── test_theme.cpp
    └── test_icons.cpp

5.2 完整 CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(MiniStyle VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

# ---- Qt 版本选择 ----
set(QT_VERSION "6" CACHE STRING "Qt major version (5 or 6)")

if(QT_VERSION STREQUAL "5")
    find_package(Qt5 COMPONENTS Widgets Svg REQUIRED)
    set(QT_WIDGETS Qt5::Widgets)
    set(QT_SVG     Qt5::Svg)

    function(mini_add_library name)
        add_library(${name} ${ARGN})
    endfunction()
else()
    find_package(Qt6 COMPONENTS Widgets Svg SvgWidgets REQUIRED)
    set(QT_WIDGETS Qt6::Widgets)
    set(QT_SVG     Qt6::Svg Qt6::SvgWidgets)

    function(mini_add_library name)
        qt_add_library(${name} ${ARGN})
    endfunction()
endif()

# ---- 资源文件(可选) ----
option(MINI_BUILD_RESOURCES "Build with built-in resources" ON)
if(MINI_BUILD_RESOURCES)
    set(RESOURCE_FILES resources/resources.qrc)
endif()

# ---- 核心库 ----
mini_add_library(miniStyle STATIC
    src/MiniStyle.hpp
    src/MiniStyle.cpp
    src/MiniStylePlugin.cpp
    src/ThemeManager.hpp
    src/ThemeManager.cpp
    src/IconEngine.hpp
    src/IconEngine.cpp
    src/AnimationEngine.hpp
    src/AnimationEngine.cpp
    src/QtCompat.hpp
    src/StyleExtension.hpp
    ${RESOURCE_FILES}
)

target_include_directories(miniStyle
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
        $<INSTALL_INTERFACE:include/MiniStyle>
)

target_link_libraries(miniStyle
    PUBLIC
        ${QT_WIDGETS}
        ${QT_SVG}
)

target_compile_definitions(miniStyle PRIVATE MINI_STYLE_LIBRARY)

# ---- 安装 ----
include(GNUInstallDirs)
install(TARGETS miniStyle
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(DIRECTORY src/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/MiniStyle
    FILES_MATCHING PATTERN "*.hpp"
)

# ---- 示例 ----
option(MINI_BUILD_EXAMPLES "Build example applications" OFF)
if(MINI_BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

# ---- 测试 ----
option(MINI_BUILD_TESTS "Build tests" OFF)
if(MINI_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

5.3 代码量估算

模块 行数 说明
MiniStyle.hpp/cpp ~250 主绘制入口,枚举分发
ThemeManager.hpp/cpp ~150 JSON 主题加载、信号
IconEngine.hpp/cpp ~120 SVG 渲染、缓存、着色
AnimationEngine.hpp/cpp ~100 过渡动画
QtCompat.hpp ~150 Qt5/Qt6 兼容
StyleExtension.hpp ~30 扩展枚举
合计 ~800 核心框架

加上 demo 和 widget 示例约 1200 行,全部可编译运行。

5.4 最小可运行示例

// examples/minimal/main.cpp
#include <QApplication>
#include <QMainWindow>
#include <QPushButton>
#include <QVBoxLayout>
#include "MiniStyle.hpp"
#include "ThemeManager.hpp"

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    // 设置自定义风格
    app.setStyle(new MiniStyle());

    // 加载暗色主题
    ThemeManager::instance().setThemeJsonPath(":/themes/dark.json");

    QMainWindow window;
    auto *central = new QWidget;
    auto *layout  = new QVBoxLayout(central);

    auto *btn = new QPushButton("Hello MiniStyle");
    btn->setMinimumSize(200, 60);
    layout->addWidget(btn);

    window.setCentralWidget(central);
    window.setWindowTitle("MiniStyle Demo");
    window.resize(400, 300);
    window.show();

    return app.exec();
}

6. 参考:完整兼容宏速查

差异点 Qt5 Qt6 兼容方案
枚举类型 flat enum enum class 命名空间常量别名 / #if
QIcon::pixmap() (size, mode, state) (size, dpr) compatIconPixmap()
QVariant::type() .type() .typeId() .metaType().id() 通用
enterEvent QEvent* QEnterEvent* #if 宏 + compatEnterPos()
QStyleOptionTab::tabIndex 需 V4 版本 基类内置 CompatTabOption 别名
QRect::toRectF() 不存在 存在 QRectF(rect) 通用
QMenu::addAction() (text, recv, slot, key) (text, key, recv, slot) compatAddAction()
add_library 标准 CMake qt_add_library() 构建脚本分支
QRegularExpression 可选 强制(无 QRegExp) 统一用 QRegularExpression

总结

六篇文章,从零搭建了一个完整的 Qt 自定义 UI 框架:

  1. 基础:QStyle 架构与绘制流程
  2. 主题:JSON 驱动的设计令牌系统
  3. 布局:polish/eventFilter 的灵活应用
  4. (留作占位)
  5. 动画:状态过渡与缓动函数
  6. 集成(本文):扩展枚举、SVG 图标、Qt5/Qt6 兼容、完整架构

框架虽小(~800 行核心代码),但覆盖了真实项目中 90% 的自定义 UI 需求。关键是理解 Qt 的绘制管线——剩下的都是工程细节。

全系列代码地址:github.com/logiclabs/mini-style(MIT License)