引言
前五篇文章从 QStyle 基础到动画系统,逐步构建了一个可用的自定义 UI 框架。但实用的框架还需要三块拼图:扩展控件绘制能力、现代化的图标系统、以及 Qt5/Qt6 跨版本兼容。本文把这些内容一次性讲清楚。
读完你将能:
- 在不破坏 Qt 原生枚举的前提下扩展 QStyle 的绘制元素
- 实现一个支持自动着色、像素级缓存的 SVG 图标系统
- 用 CMake 同时支持 Qt5 和 Qt6 编译
- 写出零运行时开销的 Qt 版本兼容层
1. 扩展 QStyle:自定义枚举与虚函数
QStyle 的绘制能力通过 PrimitiveElement、ControlElement、PixelMetric 等枚举来索引。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?
- 隔离性:自定义枚举值与 Qt 版本解耦,升级 Qt 不会引入冲突。
- 类型安全:
enum class无法隐式转换,编译器帮你检查传参错误。 - 清晰边界:一眼就知道哪些是"我们画的”,哪些是 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 ¤tTheme() 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 框架:
- 基础:QStyle 架构与绘制流程
- 主题:JSON 驱动的设计令牌系统
- 布局:polish/eventFilter 的灵活应用
- (留作占位)
- 动画:状态过渡与缓动函数
- 集成(本文):扩展枚举、SVG 图标、Qt5/Qt6 兼容、完整架构
框架虽小(~800 行核心代码),但覆盖了真实项目中 90% 的自定义 UI 需求。关键是理解 Qt 的绘制管线——剩下的都是工程细节。
全系列代码地址:github.com/logiclabs/mini-style(MIT License)