做 Qt/C++ 客户端开发这些年,界面皮肤一直是我最不想碰的模块。倒不是 QSS 有多难写,而是当你面对几十个控件类、十几个状态组合时,改一个主题色的工作量会呈指数级上涨。后来我干脆自己折腾了一套 Qt/C++皮肤生成器,把颜色、圆角、间距、字体全部抽成配置,运行时一键应用、实时预览,彻底把换肤这件事从代码里解放出来。这篇文章会把整体架构、核心实现和踩过的坑逐一讲清楚,适合那些觉得默认样式太死板、又不想靠“搜索-复制”式改 QSS 的 Qt 开发者。
如果你只是偶尔想给按钮换个颜色,那这套方案可能有点“杀鸡用牛刀”;但如果你的项目已经开始出现三套以上的配色、或者设计师每周都在调视觉细节,那这套方案能让你的维护成本直接降一个量级。我本着“先解决自己的问题,再抽象成通用能力”的思路来做,所以下面所有内容都是从一个真实项目里抽出来的,不是纸上谈兵。
1. 手写QSS的维护成本,以及生成器要解决的事
1.1 一个按钮四种状态,把颜色矩阵摊开给你看
先看一个最普通的场景:一个主按钮。正常态背景是#2D8CF0,鼠标悬停变成#57A3F3,按下变成#1E6EC7,禁用变成#A0CFFF。这四个颜色在 QSS 里并不难写:
QPushButton { background-color: #2D8CF0; } QPushButton:hover { background-color: #57A3F3; } QPushButton:pressed { background-color: #1E6EC7; } QPushButton:disabled { background-color: #A0CFFF; }问题在于,真正的应用里主色不只出现在按钮上。选中列表项要使用它,链接文字要使用它,进度条的填充要使用它,输入框聚焦时的边框要使用它,Tab 标签的选中态要使用它,甚至图标 hover 的背景也要使用它。也就是说,一个primaryColor会被复制到十几个 QSS 规则里。一旦产品经理说“蓝色不够商务,换成深青色吧”,你要面对的不是一处替换,而是全应用范围内的颜色矩阵清洗。
这种痛和代码里的“魔法数字”如出一辙:你当然可以直接替换,但漏掉一个#2D8CF0的变体(比如某个地方手工调淡了 10%),视觉上就会出现一颗“钉子户”按钮。手写 QSS 最消耗精力的不是语法,而是这些语义颜色无法被统一管理。皮肤生成器真正要解决的,就是这件事。
1.2 生成器的边界:管配置,不管微观视觉
一开始我以为皮肤生成器应该是一个能把任意 QSS 可视化编辑出来的大工具,后来想明白了,这个边界划得太大会把自己拖死。我把它的能力收敛成四块:主题配置化、动态加载、实时预览、统一刷新。
- 主题配置化:所有颜色、尺寸、字体抽成结构化的配置,而不是散落在 QSS 里的硬编码值。
- 动态加载:运行时不重启程序就能切换皮肤,支持从外部导入新皮肤。
- 实时预览:面向设计师提供一个控件画廊,调参后立刻看到效果。
- 统一刷新:切换主题后,所有窗口、所有控件一次刷新,不出现“新旧混杂”。
它也明确不管什么:不管单条 QSS 规则怎么写,不管某个控件的微观视觉细节怎么调。这些仍然是 QSS 的老玩家该做的事。生成器只是把高层主题配置“翻译”成底层 QSS,相当于在前端领域常见的 CSS 变量,只不过 Qt 的 QSS 不支持变量,所以我们要自己做一层预处理器。
这个边界想清楚之后,整套代码的结构就非常清晰了:配置是第一层,编译是第二层,应用和刷新是第三层。
2. 主题数据模型与QSS编译链路的设计
2.1 用JSON描述主题:从“颜色值”到“语义颜色”
我最终选择用 JSON 作为主题描述文件。原因很简单:JSON 可读性好、层级清晰、支持数组和对象,Qt 客户端解析起来也不麻烦。每个主题文件长这样:
{ "id": "ocean-blue", "name": "海蓝商务", "colors": { "bg": "#F5F7FA", "panel": "#FFFFFF", "primary": "#2D8CF0", "primaryHover": "#57A3F3", "primaryPressed": "#1E6EC7", "primaryDisabled": "#A0CFFF", "text": "#333333", "textSecondary": "#666666", "border": "#DCDFE6", "success": "#18A058", "warning": "#F0A020", "danger": "#D03050", "scrollbarTrack": "#F0F2F5", "scrollbarHandle": "#C0C4CC", "scrollbarHandleHover": "#909399" }, "metrics": { "radius": 6, "padding": 8, "spacing": 4, "iconSize": 16, "scrollbarWidth": 10 }, "fonts": { "family": "Microsoft YaHei", "size": 13 } }注意我特意用了语义化命名,比如primaryHover、primaryPressed,而不是blue1、blue2。这是给设计师看的关键一步。当设计师拿到这份 JSON,他能立刻理解每个字段控制的是什么状态;当开发在 QSS 模板里看到@primaryHover,也知道这个值是从哪里来的。主题开发最难的不是写代码,而是让非程序员也能参与,语义化命名是第一步。
2.2 QSS模板与占位符替换,而不是直接拼字符串
有了 JSON 配置,下一步就是把配置变成可执行的 QSS。如果直接在代码里用字符串拼接生成整份 QSS,模板的维护性会非常差,你会在 C++ 源码里看到成片的 QSS,转义、引号、换行全是灾难。所以我选择用“QSS模板 + 占位符”的方案。
模板放在独立的.qss文件里,内容类似这样:
QMainWindow, QDialog { background-color: @bg; } QPushButton { background-color: @primary; color: #FFFFFF; border: none; border-radius: @radius; padding: @padding @padding; } QPushButton:hover { background-color: @primaryHover; } QPushButton:pressed { background-color: @primaryPressed; } QPushButton:disabled { background-color: @primaryDisabled; color: #D0D0D0; } QScrollBar:vertical { background: @scrollbarTrack; width: @scrollbarWidth; }注意@primary、@radius这样的占位符,它们就是配置和 QSS 之间的桥梁。编译阶段只需要把配置里的值替换到占位符里即可。这个方案最大的好处是:QSS 规则本身仍然集中在一个文件里,可以用你熟悉的语法写;颜色和尺寸参数全部来自 JSON,不会出现多处维护。
2.3 ThemeManager:为什么换肤入口必须是全局单例
皮肤系统在项目里的定位必须是全局唯一的,原因很朴素:样式表是QApplication级别的属性,如果你每个窗口都自己管一份皮肤状态,早晚会出现两个窗口主题不一致的情况。我设计了一个ThemeManager单例,所有换肤操作都走这一个入口:
class ThemeManager : public QObject { Q_OBJECT public: static ThemeManager *instance(); void loadTheme(const QString &filePath); void applyTheme(const QString &themeId); QString currentThemeId() const; QColor currentColor(const QString &key) const; int currentMetric(const QString &key) const; signals: void themeChanged(const QString &themeId); private: ThemeManager(QObject *parent = nullptr); QString compileTheme(const SkinTheme &theme) const; void repolishAllWidgets(); QMap<QString, SkinTheme> themes_; QString currentThemeId_; QHash<QString, QString> compiledQssCache_; };单例模式在这个场景下不是过度设计,而是必要约束。themeChanged信号供业务模块监听,比如某些控件切换主题后需要换图标、改特殊属性,窗口自己处理这部分即可。有同学会问,为什么不直接用qApp->setStyleSheet就完事了?因为缺少状态管理,你无法知道当前是哪个主题、也无法回退到上一个主题。有了ThemeManager,这些状态都收口了。
3. 核心代码实现:动态换肤的完整流程
3.1 编译主题与缓存:把QSS从配置里“算”出来
编译函数是整个生成器的核心。思路不复杂,用一个QHash存变量名和值的映射,然后遍历模板做替换。但这里有一个非常容易踩的隐藏坑,我一开始就被它坑过:如果先替换@primary,再替换@primaryHover,那么模板里的@primaryHover会先被替换成#2D8CF0Hover,后面的替换根本匹配不上。
正确做法是按占位符长度降序替换,或者使用正则表达式一次性匹配所有@变量名再替换。我最终用的方案是正则匹配:
QString ThemeManager::compileTheme(const SkinTheme &theme) const { QString qss = theme.qssTemplate; // 建立变量映射 QHash<QString, QString> variables; for (auto it = theme.colors.cbegin(); it != theme.colors.cend(); ++it) { variables.insert(it.key(), it.value().name(QColor::HexArgb)); } variables.insert("radius", QString::number(theme.metrics.radius) + "px"); variables.insert("padding", QString::number(theme.metrics.padding) + "px"); variables.insert("scrollbarWidth", QString::number(theme.metrics.scrollbarWidth) + "px"); variables.insert("fontSize", QString::number(theme.fontSize) + "px"); // 用正则匹配 @xxx,避免占位符互相包含 QRegularExpression re("@([a-zA-Z0-9]+)"); QRegularExpressionMatchIterator iter = re.globalMatch(qss); QStringList capturedKeys; while (iter.hasNext()) { auto match = iter.next(); capturedKeys << match.captured(1); } // 从后往前替换,避免迭代器失效 for (int i = capturedKeys.size() - 1; i >= 0; --i) { const QString key = capturedKeys.at(i); if (variables.contains(key)) { qss.replace("@" + key, variables.value(key)); } } return qss; }这段代码还有一个细节:把匹配到的所有 key 先收集到capturedKeys,再从后往前替换,是因为正则迭代器在替换过程中会失效,从后往前可以规避这个问题。实际工程中我建议把编译结果缓存到compiledQssCache_,同一个主题只编译一次。QSS 模板一般都有几百行,每次切换主题都重新解析全部模板没有必要。
3.2 QProxyStyle接管原生绘制:QSS覆盖不到的部分
我一开始以为 QSS 能覆盖所有视觉,后来发现这是错觉。QSS 本质上是 Qt 样式系统(尤其是 Fusion 风格)之上的样式表层,它能改颜色、能改图片、能改部分子控件,但有些元素它根本碰不到,比如滚动条的间隙计算、某些平台下菜单项的点击动画、复杂控件内部的状态布局。想用纯 QSS 实现那种“圆角滑块 + 渐变轨道 + 悬停高亮”的自定义滚动条,你会做得非常痛苦。
这里就要引入QProxyStyle。它和QStyleSheetStyle不是替代关系,而是互补关系。在 main 函数里先挂一层代理:
int main(int argc, char *argv[]) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication app(argc, argv); app.setStyle(new AppStyle("Fusion")); return app.exec(); }然后在AppStyle里接管原生绘制。比如滚动条,我选择接管CC_ScrollBar:
class AppStyle : public QProxyStyle { Q_OBJECT public: using QProxyStyle::QProxyStyle; void drawComplexControl(ComplexControl control, const QStyleOptionComplex *option, QPainter *painter, const QWidget *widget) const override { if (control == CC_ScrollBar) { const auto *slider = qstyleoption_cast<const QStyleOptionSlider *>(option); if (slider) { drawCustomScrollBar(slider, painter); return; } } QProxyStyle::drawComplexControl(control, option, painter, widget); } int pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const override { if (metric == PM_ScrollBarExtent) { return ThemeManager::instance()->currentMetric("scrollbarWidth"); } return QProxyStyle::pixelMetric(metric, option, widget); } private: void drawCustomScrollBar(const QStyleOptionSlider *slider, QPainter *painter) const { ThemeManager *tm = ThemeManager::instance(); const QColor trackColor = tm->currentColor("scrollbarTrack"); const QColor handleColor = tm->currentColor("scrollbarHandle"); const QColor handleHoverColor = tm->currentColor("scrollbarHandleHover"); painter->save(); painter->setRenderHint(QPainter::Antialiasing); // 绘制轨道 painter->fillRect(slider->rect, trackColor); // 绘制滑块 QRect handleRect = slider->rect; if (slider->orientation == Qt::Horizontal) { int handleWidth = slider->pageStep * slider->rect.width() / qMax(1, slider->maximum + slider->pageStep); handleRect.setWidth(handleWidth); handleRect.moveLeft(slider->sliderPosition * slider->rect.width() / qMax(1, slider->maximum + slider->pageStep)); } painter->setBrush(handleColor); painter->setPen(Qt::NoPen); painter->drawRoundedRect(handleRect, 5, 5); // hover 状态这里省略 painter->restore(); } };这块代码的价值在于:皮肤的某些物理参数不再受 QSS 表达能力限制,而是直接和ThemeManager里的配置绑定。以后设计师说滚动条要更宽一点,改 JSON 里的scrollbarWidth就可以了,QProxyStyle 会自动生效。
3.3 运行时刷新:repolish与update的正确顺序
主题切换后最常见的问题就是“样式没变全”:有些按钮换过来了,有些控件还停留在旧皮肤。这往往不是 QSS 没应用,而是没有强制控件重新解析自己的样式表。qApp->setStyleSheet会触发一次全局样式重算,但有些控件因为缓存了旧的样式状态,或者是在样式切换后被创建,不会自动更新。
我封装了一个刷新函数:
void ThemeManager::repolishAllWidgets() { const QWidgetList allWidgets = QApplication::allWidgets(); for (QWidget *widget : allWidgets) { QStyle *style = widget->style(); style->unpolish(widget); style->polish(widget); widget->update(); } }调用顺序是unpolish->polish->update。unpolish把控件旧的样式信息清掉,polish按新的样式表重新计算,update触发重绘。注意,如果只调用update,可能只是重绘,但控件内部缓存的边距、字体、调色板仍然是旧的。还有一个细节:如果皮肤切换后控件尺寸发生了变化,比如滚动条变宽了,需要调用widget->updateGeometry()甚至widget->adjustSize(),否则布局不会重新计算。
这里还必须提一个QWidget的经典属性——Qt::WA_StyledBackground。如果你发现某些QWidget设置了背景色但没生效,先检查这个属性是否设置。QSS 对普通QWidget的背景绘制默认不走样式表,除非设置了该属性:
widget->setAttribute(Qt::WA_StyledBackground, true);在动态换肤场景里,这个问题很容易被误判为“系统 bug”,实际上只是QWidget对背景绘制的限制。
4. 落地到真实项目:资源、预览与性能
4.1 主题资源怎么组织:qrc内置还是外部目录加载
主题的存放方式,直接影响发布和运营。我的策略是“默认内置 + 外部覆盖”。
默认皮肤放进.qrc资源文件:
<RCC> <qresource prefix="/themes"> <file alias="ocean-blue.json">themes/ocean-blue.json</file> <file alias="ocean-blue.qss">themes/ocean-blue.qss</file> <file alias="midnight-purple.json">themes/midnight-purple.json</file> <file alias="midnight-purple.qss">themes/midnight-purple.qss</file> </qresource> </RCC>这样程序安装后自带一套完整可用的皮肤,不依赖外部文件。同时,在程序数据目录(比如QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))下建立themes子目录,扫描里面的*.json,如果发现id和内置主题冲突,外部文件优先。这相当于给皮肤开了一个“插件口”,运营后续要加一套节日主题,不需要重新发版本,丢一个 JSON 文件进去就行。
这个设计和浏览器扩展很像:核心皮肤内置保证基础体验,外部目录保证可扩展。唯一的坑是外部 JSON 解析失败时要有回退逻辑,我一般这样处理:
void ThemeManager::loadThemeFromFile(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "open theme file failed:" << filePath; return; } QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); file.close(); if (error.error != QJsonParseError::NoError || !doc.isObject()) { qWarning() << "theme json parse error:" << error.errorString(); return; } SkinTheme theme = parseTheme(doc.object()); if (theme.id.isEmpty()) { qWarning() << "theme id is empty, ignore"; return; } themes_.insert(theme.id, theme); }注意主题 id 为空时直接忽略,不然切换主题时会指向一个无法定位的资源,问题排查起来很费劲。
4.2 实时预览窗口:让设计师自己动手调皮肤
皮肤生成器如果不带预览窗口,就只能算一个配置文件加载器。真正的体验提升,来自一个“控件画廊”预览页。我在项目里抽了一个SkinPreviewWindow,把应用里最常见的二十几种控件全部铺在一个页面上:按钮(常规、悬停、按下、禁用)、输入框、下拉框、复选框、单选按钮、列表、表格、Tab 标签、进度条、滚动条、菜单、滑动条,甚至包括自定义的QWidget面板。
预览窗口本身不参与业务逻辑,它只做一件事:展示当前主题在典型场景下的表现。设计师调整 JSON 里的颜色值或尺寸值后,点击“应用”,ThemeManager::applyTheme被调用,预览窗口立刻刷新。这样设计师可以边调边看,不用等开发同学手动改 QSS 再编译再运行。
这个窗口的构造难度很低,但不建议用业务页面做预览。业务页面控件类型少、状态覆盖不全,而且可能会因为网络请求或数据加载问题影响预览效果。一个纯粹的、固定的控件画廊反而是最稳定的皮肤“测试床”。
4.3 主题切换的节流策略与性能观察
在预览窗口里如果放一个颜色编辑器,让用户拖颜色滑块时实时看效果,你会发现一个问题:滑块每动一个像素都会触发一次applyTheme,每次都是全量setStyleSheet+repolishAllWidgets,在控件数量超过三百个的页面上,帧率会明显下降,甚至出现闪烁。
解决思路是节流。我用一个QTimer做去抖,用户连续拖动滑块时,只有停止拖动 200 毫秒后才真正应用主题:
QTimer *debounceTimer = new QTimer(this); debounceTimer->setSingleShot(true); debounceTimer->setInterval(200); connect(debounceTimer, &QTimer::timeout, this, [this]() { ThemeManager::instance()->applyTheme(currentPreviewThemeId_); }); // 预览窗口拖颜色滑块时的处理函数 void SkinPreviewWindow::onColorChanged(const QString &key, const QColor &color) { // 只更新预览缓存里的颜色,不急着全量应用 previewTheme_.colors[key] = color; debounceTimer->start(); }除了节流,还有一个性能经验:全量qApp->setStyleSheet的耗时和控件数量强相关,但在几百个控件的窗口上一般不会超过 100ms,在用户交互间隙做切换是可接受的。真正的性能杀手是“频繁小切换”,所以我的原则是:交互中预览用局部刷新,交互结束才全量应用。
5. 三组高频Bug的定位过程与修复方案
5.1 子控件样式不生效:选择器继承与优先级
我在测试皮肤生成器时遇到一个非常典型的问题:给QWidget设置了背景色后,里面的QLabel文字颜色死活不按模板来。排查到最后发现,问题出在很多人对 QSS 选择器继承机制的理解上。
QSS 里的选择器继承和 C++ 对象树继承不是一回事。QWidget { color: red; }这条规则,并不会自动让里面所有子控件的文字都变红,它影响的只是QWidget自身以及那些从 QSS 中继承样式的属性。更常见的问题反而是你设置了QWidget { background-color: @bg; },导致所有QWidget子类都去绘制背景,一些本来透明的容器变得不透明。
正确做法的原则是“选择器越具体越好”。比如给特定页面设置背景,应该用 objectName 限定:
QWidget#pageContainer { background-color: @bg; } QWidget#pageContainer QLabel { color: @text; background: transparent; }另外还要记住 QSS 的优先级顺序:控件上的局部setStyleSheet优先于全局qApp->setStyleSheet,objectName选择器优先于类选择器。皮肤生成器要想全局生效,必须和团队约定:不要在局部窗口里写硬编码的setStyleSheet,至少不要让局部样式表覆盖皮肤管理的颜色。否则换肤后某个窗口会“顽固不化”。
5.2 换肤后“新旧混杂”的完整排查链路
“主窗口已经是新皮肤,弹窗还是旧皮肤”是我在开发过程中接到最多的反馈。有一次客户截图给我看,主界面是深色皮肤,弹出的设置窗口却还是浅色,截图里外对比非常刺眼。
我当时的排查链路是这样的:
- 先确认全局样式表确实变了:打印
qApp->styleSheet().length(),看内容和长度是否匹配目标主题。 - 检查弹窗对象创建时机:如果弹窗是在换肤之后才创建,理论上应该默认继承全局样式,除非它自己设置了局部样式。于是打印
widget->styleSheet(),果然发现弹窗构造函数里写了一行setStyleSheet("background: #FFFFFF;")。 - 修复方式不是删掉这行就完事,而是把它替换成从
ThemeManager获取对应语义色,或者干脆删除局部样式,统一走全局模板。
这个案例说明“新旧混杂”九成都是局部样式覆盖全局导致的。还有一个隐蔽场景:动态属性(Dynamic Property)参与 QSS 选择器。比如某个按钮通过setProperty("level", "primary")控制样式,换肤后如果属性没重新设置,它的样式也会停留在上一次的状态。建议动态属性变化后主动触发一次style()->unpolish(widget); style()->polish(widget); widget->update();。
下面的表格是常用排查参考:
| 现象 | 可能原因 | 定位手段 | 修复方向 |
|---|---|---|---|
| 主窗口新样式、弹窗旧样式 | 弹窗构造函数有局部setStyleSheet | 检查弹窗源码widget->styleSheet() | 删除局部样式,改走全局皮肤 |
| 背景没变但文字变了 | QWidget缺少WA_StyledBackground | 查看控件属性 | 设置setAttribute(Qt::WA_StyledBackground, true) |
| 部分控件颜色怪 | 旧动态属性残留 | 打印dynamicPropertyNames | 换肤时统一清除并重设动态属性 |
| 切换后控件尺寸没变 | 只update未updateGeometry | 观察布局是否重排 | 调用widget->updateGeometry() |
5.3 平台差异与高分屏下的表现处理
Qt 应用免不了要跨平台跑,皮肤系统在 Windows、macOS、Linux 三方平台的表现差异,比预想的大得多。
macOS 上最大的坑是按钮焦点框。默认的QPushButton在获取焦点时会出现一圈蓝色光晕,这是苹果平台的特性,QSS 很难完全去掉。需要在按钮初始化时设置:
button->setAttribute(Qt::WA_MacShowFocusRect, false);Linux 上则要小心平台主题的干扰。某些桌面环境(比如 GNOME/ KDE 的主题)会接管 Qt 控件的部分绘制,导致皮肤在开发机上完美、在客户的 Linux 机器上乱掉。我的建议是应用启动时显式固定一个基础 QStyle,再叠加上QProxyStyle,避免依赖平台原生主题。
高分屏是另一个需要特别留心的地方。Qt 6 和 Qt 5.15 以上默认启用了高 DPI 缩放,QSS 里的border-radius: 6px是逻辑像素,Qt 内部会做缩放,一般不会出现问题。真正容易出问题的是你在QProxyStyle或自定义绘制里直接操作坐标时,错误地手动乘以devicePixelRatio,结果反而导致尺寸翻倍、边缘模糊。我的经验是:只要你的绘制目标是QWidget,Qt 的QPainter已经工作在逻辑坐标系,不需要手动处理 DPI;只有当你离线渲染到QImage或QPixmap、再贴到界面上时,才需要显式设置pixmap.setDevicePixelRatio(ratio)。
6. 从换肤工具走向皮肤生态的扩展路径
6.1 皮肤导入导出与用户自定义
当皮肤系统稳定之后,下一步自然是放开给用户自己做皮肤。我在工程里加了导出功能:把当前配置和模板打成一个 zip 包,里面包含 JSON 配置、QSS 模板和预览截图。用户拿到这个包,可以自己解压改配置,再通过“导入皮肤”功能加载。
导入时一定要做校验,不要相信任何外部输入。我的校验表很简单:JSON 能否被解析、主题 id 是否为空、颜色字段是否是合法色值、尺寸字段是否是正数。只要有一项不满足,就拒绝导入并给出明确错误提示。这样既保证了安全性,又能让用户在尝试自定义主题时得到即时反馈。
对普通用户来说,直接编辑 JSON 还是有点门槛,于是我把“调整主题变量”做成了配置界面:左边一列语义颜色,右边一个色块,点击后弹出颜色选择对话框。本质上是一个简化版的可视化编辑器,但因为它和皮肤系统的数据模型完全绑定,所以改一个变量立刻能看到效果。
6.2 从配置到可视化编辑器:下一步打算
我现在正在做的是把预览窗口升级成真正的可视化皮肤编辑器,能做到拖动色相环、调整透明度、实时看控件画廊变化。技术上的核心难点反而不是编辑器本身,而是如何让“系统级 QProxyStyle 绘制”这块也能实时响应预览。目前我的做法是在AppStyle里不直接读ThemeManager的当前主题,而是提供一个previewSkin_指针,预览模式下优先读预览配置。这样切换预览值时同样会被QProxyStyle的绘制代码读取,滚动条、菜单等 QSS 无法覆盖的控件也能实时反馈。
我自己的体会是,皮肤系统一旦跑通,后续的日间/夜间模式、节假日主题、品牌定制都变成了同一条链路。前期花在数据模型上的那些设计,会在每一次新增主题时翻倍回报回来。如果你也准备给 Qt 项目做换肤,我建议先把“语义化配置 + QSS 模板 + 全局单例”这条主干搭起来,预览窗口先放一个朴素的控件画廊就够用,等验证了这套模式的价值,再慢慢往编辑器方向扩展也不迟。