1. 设计哲学:数据与绘制分离

传统自定义 QStyle 的做法是什么?直接在 drawControl() 里写死颜色。

void MyStyle::drawControl(...) {
    QColor bg = QColor("#1e1e1e"); // 硬编码
    QColor fg = QColor("#d4d4d4");
    painter->fillRect(rect, bg);
    painter->setPen(fg);
    // ...
}

当你需要深色/浅色切换,或者要套用另一套品牌色时,噩梦就开始了——你得翻遍几十个 draw 函数一个个改。这是 绘制逻辑和视觉参数耦合 的典型症状。

Theme 系统的核心理念很简单:

颜色、尺寸、字体这些"数据"集中管理;绘制函数只负责"怎么画”,不关心"画什么颜色”。

┌─────────────────────────────────────────────┐
│                 Theme (数据层)                │
│  colors / sizes / fonts / spacings           │
└──────────────┬──────────────────────────────┘
               │ themeChanged() signal
               ▼
┌─────────────────────────────────────────────┐
│              QStyle (绘制层)                  │
│  只做 painting,通过 theme() 拿当前色值       │
└─────────────────────────────────────────────┘

这样做有三个直接好处:

  1. 主题切换零代码改动——换掉 Theme 对象,全界面自动刷新。
  2. 品牌定制只需一套 JSON——甲方要蓝色主色调?改 primaryColor 一个字段就行。
  3. 分支隔离清晰——UI 开发者不用碰 QStyle 代码也能调主题色。

2. 颜色层次模型:四层体系

一个成熟的 UI 框架至少需要 20~40 个设计 token。我们把它们组织成四层,每层有明确的职责边界。

2.1 基础调色板(Base Palette)

界面背景色,通常 4~5 个层次灰度,用于区分容器深度。

QColor backgroundColorMain1;  // 最底层(窗体背景)
QColor backgroundColorMain2;  // 卡片/面板背景
QColor backgroundColorMain3;  // 可交互区域(列表行、输入框底色)
QColor backgroundColorMain4;  // 悬浮层/分隔线
QColor neutralColor;           // 中性文字色(placeholder、禁用态文字)

层叠关系: Main1 最深 → Main4 最浅(深色主题反之)。这就是 Material Design 里"elevation"概念的简化实现——不需要阴影,靠颜色层次就能区分容器。

2.2 品牌色(Brand Colors)

QColor primaryColor;
QColor primaryColorHovered;
QColor primaryColorPressed;
QColor primaryColorDisabled;

QColor secondaryColor;
QColor secondaryColorHovered;
QColor secondaryColorPressed;
QColor secondaryColorDisabled;

Primary 是主按钮、选中态、焦点等"强调"用色。Secondary 是辅助操作色。两者各自携带 Normal / Hovered / Pressed / Disabled 四态变体。

2.3 语义色(Semantic Colors)

字段 用途
statusColorSuccess 成功提示、绿色标签
statusColorInfo 信息提示、蓝色标签
statusColorWarning 警告提示、橙色标签
statusColorError 错误提示、红色标签
borderColor 常规边框色
focusColor 焦点指示器颜色(通常即 primaryColor)
shadowColor 阴影色(通常带 alpha 的黑色)

语义色不参与 base palette 的层级逻辑。它们只管"状态传达”——无论深色浅色主题,红色就该表示错误。

2.4 透明变体(Alpha Variants)

QColor backgroundColorMain1Transparent;  // ≈ backgroundColorMain1 + alpha≈0.5
QColor backgroundColorMain2Transparent;
QColor backgroundColorMain3Transparent;
QColor backgroundColorMain4Transparent;
QColor neutralColorTransparent;
QColor primaryColorTransparent;
QColor secondaryColorTransparent;

用途:tooltip 背景、modal overlay、水印效果。为什么不直接 color.setAlpha()?因为 JSON 序列化要求存完整 #RRGGBBAA,取用零开销,而且语义明确——读代码的人一眼知道这是"基础色的半透明版本”。

qlementine 参考:Theme.hpp 的完整字段列表,实际设计中 transparent 变体是自动计算(或从 JSON 读)的,不需要独立维护两套色值。

3. 交互四态:Normal → Hovered → Pressed → Disabled

这是 GUI 框架最容易做烂的部分。很多项目只有 Normal 和 Disabled,Hovered 和 Pressed 靠代码里临时调 lighter()/darker()。这种方式有两个致命问题:

  1. 不可预测——darker(120) 在浅色和深色主题下效果完全不同。
  2. 不可定制——设计师说"hover 态加 10% 饱和度”,你没法在代码里精确表达。

Theme 系统的做法是:每种品牌色显式定义四态,禁止运行时代数运算。

// 以 primary 系列为例,QStyle 中的取色逻辑:
QColor styleColor(ThemeColor role, InteractionState state) {
    switch (role) {
    case Primary:
        switch (state) {
        case Normal:   return theme.primaryColor;
        case Hovered:  return theme.primaryColorHovered;
        case Pressed:  return theme.primaryColorPressed;
        case Disabled: return theme.primaryColorDisabled;
        }
    case Secondary:
        // ...
    }
}

那么 hover 态是谁触发的?QStyle 的 polish() + QEvent::HoverEnter 我们在 widget property 上标记 hovered = true,重绘时 Style 检查这个 property 再选择对应颜色。

// 概念示意:
void MyStyle::polish(QWidget* w) {
    w->installEventFilter(this);
    w->setProperty("hovered", false);
}

bool MyStyle::eventFilter(QObject* obj, QEvent* ev) {
    if (ev->type() == QEvent::HoverEnter)
        obj->setProperty("hovered", true);
    else if (ev->type() == QEvent::HoverLeave)
        obj->setProperty("hovered", false);
    return QProxyStyle::eventFilter(obj, ev);
}

这样整个状态驱动完全脱离绘制逻辑——Style 只是一个根据 property 查询 theme 再做 painting 的机器

4. 尺寸 Token:统一节奏

颜色统一了视觉风格,尺寸统一了视觉节奏。以下是核心 token:

// 圆角
double borderRadius;   // 卡片、面板的圆角

// 边框
double borderWidth;

// 控件高度(三种规格)
int controlHeightLarge;   // 对话框按钮、大输入框
int controlHeightMedium;  // 常规按钮、下拉框
int controlHeightSmall;   // 标签、小图标按钮

// 图标尺寸
int iconSize;

// 间距
int spacing;              // 默认间距(widget margin / layout spacing 基准)

// 字体大小(5 级)
int fontSizeH1;   // 标题(dialog title, section header)
int fontSizeH2;
int fontSizeH3;   // 正文
int fontSizeH4;   // 辅助文字
int fontSizeH5;   // 小标签、hint

为什么 H1~H5 而不是用 pt 值? 对 QStyle 来说,fontSizeH1 是一个有语义的 token,不是 16pt。浅色主题可能需要 H3=14px,深色主题可能需要 H3=15px(深色背景下同字号视觉偏小)。语义命名让你可以按主题微调,而不是全局硬编码。

实际使用中,QStyle 通过 ThemeManager::currentTheme() 拿到 token:

QFont MyStyle::standardFont(QWidget* w, Role role) {
    QFont f = w->font();
    auto& t = ThemeManager::instance().currentTheme();
    switch (role) {
    case Heading: f.setPixelSize(t.fontSizeH1); break;
    case Body:    f.setPixelSize(t.fontSizeH3); break;
    case Hint:    f.setPixelSize(t.fontSizeH5); break;
    }
    return f;
}

5. JSON 序列化 / 反序列化

QJsonDocument 是 Qt 内置的 JSON 实现,零外部依赖。Theme 类的 load/save 模式如下。

5.1 类声明

// Theme.hpp
#pragma once

#include <QColor>
#include <QString>
#include <QJsonObject>

struct Theme {
    // ── Base palette ──
    QColor backgroundColorMain1;
    QColor backgroundColorMain2;
    QColor backgroundColorMain3;
    QColor backgroundColorMain4;
    QColor neutralColor;

    // ── Brand ──
    QColor primaryColor;
    QColor primaryColorHovered;
    QColor primaryColorPressed;
    QColor primaryColorDisabled;
    QColor secondaryColor;
    QColor secondaryColorHovered;
    QColor secondaryColorPressed;
    QColor secondaryColorDisabled;

    // ── Semantic ──
    QColor statusColorSuccess;
    QColor statusColorInfo;
    QColor statusColorWarning;
    QColor statusColorError;
    QColor borderColor;
    QColor focusColor;
    QColor shadowColor;

    // ── Transparent ──
    QColor backgroundColorMain1Transparent;
    QColor backgroundColorMain2Transparent;
    QColor backgroundColorMain3Transparent;
    QColor backgroundColorMain4Transparent;
    QColor neutralColorTransparent;
    QColor primaryColorTransparent;
    QColor secondaryColorTransparent;

    // ── Sizes ──
    int    borderRadius         = 6;
    int    borderWidth          = 1;
    int    controlHeightLarge   = 40;
    int    controlHeightMedium  = 32;
    int    controlHeightSmall   = 24;
    int    iconSize             = 16;
    int    spacing              = 8;

    int    fontSizeH1 = 24;
    int    fontSizeH2 = 20;
    int    fontSizeH3 = 16;
    int    fontSizeH4 = 14;
    int    fontSizeH5 = 12;

    // ── JSON ──
    QJsonObject toJson()                    const;
    static Theme fromJson(const QJsonObject& obj);

    bool operator==(const Theme& o) const { return toJson() == o.toJson(); }
    bool operator!=(const Theme& o) const { return !(*this == o); }
};

5.2 序列化实现(toJson)

// Theme.cpp
#include "Theme.hpp"
#include <QJsonArray>

static QJsonObject colorToJson(const QColor& c) {
    QJsonObject obj;
    obj["r"] = c.red();
    obj["g"] = c.green();
    obj["b"] = c.blue();
    obj["a"] = c.alpha();
    return obj;
}

static QColor colorFromJson(const QJsonObject& obj) {
    return QColor(
        obj["r"].toInt(),
        obj["g"].toInt(),
        obj["b"].toInt(),
        obj["a"].toInt(255)
    );
}

// 批量读写辅助宏 —— 减少手写重复代码
#define THEME_COLOR_TO_JSON(json, theme, name) \
    json[QStringLiteral(#name)] = colorToJson(theme.name)

#define THEME_COLOR_FROM_JSON(json, theme, name) \
    if (json.contains(QStringLiteral(#name))) \
        theme.name = colorFromJson(json[QStringLiteral(#name)].toObject())

#define THEME_INT_TO_JSON(json, theme, name) \
    json[QStringLiteral(#name)] = theme.name

#define THEME_INT_FROM_JSON(json, theme, name) \
    if (json.contains(QStringLiteral(#name))) \
        theme.name = json[QStringLiteral(#name)].toInt()

QJsonObject Theme::toJson() const {
    QJsonObject json;

    // Colors
    THEME_COLOR_TO_JSON(json, *this, backgroundColorMain1);
    THEME_COLOR_TO_JSON(json, *this, backgroundColorMain2);
    THEME_COLOR_TO_JSON(json, *this, backgroundColorMain3);
    THEME_COLOR_TO_JSON(json, *this, backgroundColorMain4);
    THEME_COLOR_TO_JSON(json, *this, neutralColor);

    THEME_COLOR_TO_JSON(json, *this, primaryColor);
    THEME_COLOR_TO_JSON(json, *this, primaryColorHovered);
    THEME_COLOR_TO_JSON(json, *this, primaryColorPressed);
    THEME_COLOR_TO_JSON(json, *this, primaryColorDisabled);
    THEME_COLOR_TO_JSON(json, *this, secondaryColor);
    THEME_COLOR_TO_JSON(json, *this, secondaryColorHovered);
    THEME_COLOR_TO_JSON(json, *this, secondaryColorPressed);
    THEME_COLOR_TO_JSON(json, *this, secondaryColorDisabled);

    THEME_COLOR_TO_JSON(json, *this, statusColorSuccess);
    THEME_COLOR_TO_JSON(json, *this, statusColorInfo);
    THEME_COLOR_TO_JSON(json, *this, statusColorWarning);
    THEME_COLOR_TO_JSON(json, *this, statusColorError);
    THEME_COLOR_TO_JSON(json, *this, borderColor);
    THEME_COLOR_TO_JSON(json, *this, focusColor);
    THEME_COLOR_TO_JSON(json, *this, shadowColor);

    THEME_COLOR_TO_JSON(json, *this, backgroundColorMain1Transparent);
    THEME_COLOR_TO_JSON(json, *this, backgroundColorMain2Transparent);
    THEME_COLOR_TO_JSON(json, *this, backgroundColorMain3Transparent);
    THEME_COLOR_TO_JSON(json, *this, backgroundColorMain4Transparent);
    THEME_COLOR_TO_JSON(json, *this, neutralColorTransparent);
    THEME_COLOR_TO_JSON(json, *this, primaryColorTransparent);
    THEME_COLOR_TO_JSON(json, *this, secondaryColorTransparent);

    // Sizes
    THEME_INT_TO_JSON(json, *this, borderRadius);
    THEME_INT_TO_JSON(json, *this, borderWidth);
    THEME_INT_TO_JSON(json, *this, controlHeightLarge);
    THEME_INT_TO_JSON(json, *this, controlHeightMedium);
    THEME_INT_TO_JSON(json, *this, controlHeightSmall);
    THEME_INT_TO_JSON(json, *this, iconSize);
    THEME_INT_TO_JSON(json, *this, spacing);

    THEME_INT_TO_JSON(json, *this, fontSizeH1);
    THEME_INT_TO_JSON(json, *this, fontSizeH2);
    THEME_INT_TO_JSON(json, *this, fontSizeH3);
    THEME_INT_TO_JSON(json, *this, fontSizeH4);
    THEME_INT_TO_JSON(json, *this, fontSizeH5);

    return json;
}

Theme Theme::fromJson(const QJsonObject& json) {
    Theme theme;

    THEME_COLOR_FROM_JSON(json, theme, backgroundColorMain1);
    THEME_COLOR_FROM_JSON(json, theme, backgroundColorMain2);
    THEME_COLOR_FROM_JSON(json, theme, backgroundColorMain3);
    THEME_COLOR_FROM_JSON(json, theme, backgroundColorMain4);
    THEME_COLOR_FROM_JSON(json, theme, neutralColor);

    THEME_COLOR_FROM_JSON(json, theme, primaryColor);
    THEME_COLOR_FROM_JSON(json, theme, primaryColorHovered);
    THEME_COLOR_FROM_JSON(json, theme, primaryColorPressed);
    THEME_COLOR_FROM_JSON(json, theme, primaryColorDisabled);
    THEME_COLOR_FROM_JSON(json, theme, secondaryColor);
    THEME_COLOR_FROM_JSON(json, theme, secondaryColorHovered);
    THEME_COLOR_FROM_JSON(json, theme, secondaryColorPressed);
    THEME_COLOR_FROM_JSON(json, theme, secondaryColorDisabled);

    THEME_COLOR_FROM_JSON(json, theme, statusColorSuccess);
    THEME_COLOR_FROM_JSON(json, theme, statusColorInfo);
    THEME_COLOR_FROM_JSON(json, theme, statusColorWarning);
    THEME_COLOR_FROM_JSON(json, theme, statusColorError);
    THEME_COLOR_FROM_JSON(json, theme, borderColor);
    THEME_COLOR_FROM_JSON(json, theme, focusColor);
    THEME_COLOR_FROM_JSON(json, theme, shadowColor);

    THEME_COLOR_FROM_JSON(json, theme, backgroundColorMain1Transparent);
    THEME_COLOR_FROM_JSON(json, theme, backgroundColorMain2Transparent);
    THEME_COLOR_FROM_JSON(json, theme, backgroundColorMain3Transparent);
    THEME_COLOR_FROM_JSON(json, theme, backgroundColorMain4Transparent);
    THEME_COLOR_FROM_JSON(json, theme, neutralColorTransparent);
    THEME_COLOR_FROM_JSON(json, theme, primaryColorTransparent);
    THEME_COLOR_FROM_JSON(json, theme, secondaryColorTransparent);

    THEME_INT_FROM_JSON(json, theme, borderRadius);
    THEME_INT_FROM_JSON(json, theme, borderWidth);
    THEME_INT_FROM_JSON(json, theme, controlHeightLarge);
    THEME_INT_FROM_JSON(json, theme, controlHeightMedium);
    THEME_INT_FROM_JSON(json, theme, controlHeightSmall);
    THEME_INT_FROM_JSON(json, theme, iconSize);
    THEME_INT_FROM_JSON(json, theme, spacing);

    THEME_INT_FROM_JSON(json, theme, fontSizeH1);
    THEME_INT_FROM_JSON(json, theme, fontSizeH2);
    THEME_INT_FROM_JSON(json, theme, fontSizeH3);
    THEME_INT_FROM_JSON(json, theme, fontSizeH4);
    THEME_INT_FROM_JSON(json, theme, fontSizeH5);

    return theme;
}

5.3 文件级 I/O

#include <QFile>
#include <QJsonDocument>

bool saveThemeToFile(const Theme& theme, const QString& path) {
    QFile file(path);
    if (!file.open(QIODevice::WriteOnly))
        return false;

    QJsonDocument doc(theme.toJson());
    file.write(doc.toJson(QJsonDocument::Indented));
    return true;
}

std::optional<Theme> loadThemeFromFile(const QString& path) {
    QFile file(path);
    if (!file.open(QIODevice::ReadOnly))
        return std::nullopt;

    QJsonParseError err;
    QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &err);
    if (err.error != QJsonParseError::NoError)
        return std::nullopt;

    return Theme::fromJson(doc.object());
}

关于宏的使用:THEME_COLOR_TO_JSON 之类的宏大量吞掉样板代码,否则一百多行一对一字段序列化极其丑陋。这是少数"宏比模板好"的场景——纯数据映射,不需要类型多态。

6. ThemeManager:单例 + 信号驱动的热切换

Theme 只是一个数据结构。ThemeManager 提供的是:

  1. 单例持有当前 Theme
  2. load/save 包装
  3. themeChanged() 信号——这是热切换的关键
// ThemeManager.hpp
#pragma once

#include <QObject>
#include "Theme.hpp"

class ThemeManager : public QObject {
    Q_OBJECT
public:
    static ThemeManager& instance();

    const Theme& currentTheme() const { return _theme; }

    void setTheme(const Theme& theme);
    bool loadFromFile(const QString& path);
    bool saveToFile(const QString& path) const;

signals:
    void themeChanged(const Theme& newTheme);

private:
    ThemeManager() = default;
    Theme _theme;
};
// ThemeManager.cpp
#include "ThemeManager.hpp"

ThemeManager& ThemeManager::instance() {
    static ThemeManager mgr;
    return mgr;
}

void ThemeManager::setTheme(const Theme& theme) {
    if (_theme == theme) return;          // 值没变就不刷
    _theme = theme;
    emit themeChanged(_theme);
}

bool ThemeManager::loadFromFile(const QString& path) {
    auto t = loadThemeFromFile(path);
    if (!t.has_value()) return false;
    setTheme(*t);
    return true;
}

bool ThemeManager::saveToFile(const QString& path) const {
    return ::saveThemeToFile(_theme, path);
}

QStyle 的消费端:

// 在 MyStyle 构造函数里连接信号:
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
        this, [this](const Theme&) {
    // 刷新全部 widget —— 最简单的方式
    for (QWidget* w : QApplication::allWidgets())
        w->update();
});

QApplication::allWidgets() 遍历所有顶层窗口及子 widget 调用 update()——这比 QApplication::setStyle() 重建整个 style 轻量得多,因为只触发重绘,不重新 polish。

7. 代码练习:最小可运行 Theme 系统

下面给出一个极简但完整的实现。只有 11 个颜色 + 5 个尺寸 token,可以直接编译运行用来验证 Theme 切换机制。

// minimal_theme.hpp
#pragma once

#include <QColor>
#include <QString>
#include <QJsonObject>

struct MinimalTheme {
    // ── 12 个颜色 ──
    QColor bgMain1     = QColor("#1e1e1e");
    QColor bgMain2     = QColor("#252526");
    QColor bgMain3     = QColor("#2d2d30");
    QColor bgMain4     = QColor("#3e3e42");

    QColor textPrimary = QColor("#d4d4d4");
    QColor textMuted   = QColor("#8a8a8a");

    QColor accent      = QColor("#0078d4");
    QColor accentHover = QColor("#1e8ae5");
    QColor accentPress = QColor("#005a9e");
    QColor accentDisabled = QColor("#505050");

    QColor border      = QColor("#3e3e42");
    QColor success     = QColor("#4ec9b0");

    // ── 5 个尺寸 ──
    int borderRadius        = 4;
    int controlHeightMedium = 32;
    int iconSize            = 16;
    int spacing             = 8;
    int fontSizeBody        = 14;

    // ── JSON ──
    QJsonObject toJson() const;
    static MinimalTheme fromJson(const QJsonObject& obj);
    bool operator==(const MinimalTheme& o) const;
};
// minimal_theme.cpp
#include "minimal_theme.hpp"

// ── Helper ──
static QJsonObject colorJson(const QColor& c) {
    return QJsonObject{{"r", c.red()}, {"g", c.green()}, {"b", c.blue()}, {"a", c.alpha()}};
}
static QColor colorFrom(const QJsonObject& o) {
    return QColor(o["r"].toInt(), o["g"].toInt(), o["b"].toInt(), o["a"].toInt(255));
}

QJsonObject MinimalTheme::toJson() const {
    return {
        {"bgMain1",         colorJson(bgMain1)},
        {"bgMain2",         colorJson(bgMain2)},
        {"bgMain3",         colorJson(bgMain3)},
        {"bgMain4",         colorJson(bgMain4)},
        {"textPrimary",     colorJson(textPrimary)},
        {"textMuted",       colorJson(textMuted)},
        {"accent",          colorJson(accent)},
        {"accentHover",     colorJson(accentHover)},
        {"accentPress",     colorJson(accentPress)},
        {"accentDisabled",  colorJson(accentDisabled)},
        {"border",          colorJson(border)},
        {"success",         colorJson(success)},
        // sizes
        {"borderRadius",        borderRadius},
        {"controlHeightMedium", controlHeightMedium},
        {"iconSize",            iconSize},
        {"spacing",             spacing},
        {"fontSizeBody",        fontSizeBody}
    };
}

MinimalTheme MinimalTheme::fromJson(const QJsonObject& j) {
    MinimalTheme t;
    auto getColor = [&](const QString& k) -> QColor {
        return j.contains(k) ? colorFrom(j[k].toObject()) : QColor{};
    };
    t.bgMain1         = getColor("bgMain1");
    t.bgMain2         = getColor("bgMain2");
    t.bgMain3         = getColor("bgMain3");
    t.bgMain4         = getColor("bgMain4");
    t.textPrimary     = getColor("textPrimary");
    t.textMuted       = getColor("textMuted");
    t.accent          = getColor("accent");
    t.accentHover     = getColor("accentHover");
    t.accentPress     = getColor("accentPress");
    t.accentDisabled  = getColor("accentDisabled");
    t.border          = getColor("border");
    t.success         = getColor("success");

    auto getInt = [&](const QString& k, int def) -> int {
        return j.contains(k) ? j[k].toInt() : def;
    };
    t.borderRadius        = getInt("borderRadius", 4);
    t.controlHeightMedium = getInt("controlHeightMedium", 32);
    t.iconSize            = getInt("iconSize", 16);
    t.spacing             = getInt("spacing", 8);
    t.fontSizeBody        = getInt("fontSizeBody", 14);
    return t;
}

bool MinimalTheme::operator==(const MinimalTheme& o) const {
    return toJson() == o.toJson();
}
// minimal_theme_manager.hpp
#pragma once

#include <QObject>
#include "minimal_theme.hpp"

class MinimalThemeManager : public QObject {
    Q_OBJECT
public:
    static MinimalThemeManager& instance();

    const MinimalTheme& current() const { return _theme; }
    void apply(const MinimalTheme& t);
    bool loadFromFile(const QString& path);
    bool saveToFile(const QString& path) const;

signals:
    void changed(const MinimalTheme& t);

private:
    MinimalThemeManager() = default;
    MinimalTheme _theme;
};
// minimal_theme_manager.cpp
#include "minimal_theme_manager.hpp"
#include <QFile>
#include <QJsonDocument>

MinimalThemeManager& MinimalThemeManager::instance() {
    static MinimalThemeManager mgr;
    return mgr;
}

void MinimalThemeManager::apply(const MinimalTheme& t) {
    if (_theme == t) return;
    _theme = t;
    emit changed(_theme);
}

bool MinimalThemeManager::loadFromFile(const QString& path) {
    QFile f(path);
    if (!f.open(QIODevice::ReadOnly)) return false;
    QJsonParseError err;
    auto doc = QJsonDocument::fromJson(f.readAll(), &err);
    if (err.error != QJsonParseError::NoError) return false;
    apply(MinimalTheme::fromJson(doc.object()));
    return true;
}

bool MinimalThemeManager::saveToFile(const QString& path) const {
    QFile f(path);
    if (!f.open(QIODevice::WriteOnly)) return false;
    f.write(QJsonDocument(_theme.toJson()).toJson(QJsonDocument::Indented));
    return true;
}

主程序集成示例

// main.cpp
#include <QApplication>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QComboBox>
#include "minimal_theme_manager.hpp"

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

    // 1. 构造两个预设主题
    MinimalTheme dark;
    // dark 使用上面的默认值 (深色)

    MinimalTheme light;
    light.bgMain1     = QColor("#f3f3f3");
    light.bgMain2     = QColor("#ffffff");
    light.bgMain3     = QColor("#e8e8e8");
    light.bgMain4     = QColor("#d0d0d0");
    light.textPrimary = QColor("#1e1e1e");
    light.textMuted   = QColor("#666666");
    light.accent      = QColor("#0078d4");
    light.accentHover = QColor("#005a9e");
    light.accentPress = QColor("#003d6b");
    light.accentDisabled = QColor("#c0c0c0");
    light.border      = QColor("#d0d0d0");
    light.success     = QColor("#2a7d4f");

    // 2. 创建一个使用主题颜色的 widget(简化示例)
    QWidget window;
    window.resize(300, 200);

    auto& tm = MinimalThemeManager::instance();
    tm.apply(dark);

    // 连接主题变化 → 重绘
    QObject::connect(&tm, &MinimalThemeManager::changed,
                     &window, [&window]() { window.update(); });

    // 3. 切换按钮
    QComboBox* combo = new QComboBox(&window);
    combo->addItem("Dark");
    combo->addItem("Light");
    QPushButton* btn = new QPushButton("Test Button", &window);

    QVBoxLayout* lay = new QVBoxLayout(&window);
    lay->addWidget(combo);
    lay->addWidget(btn);

    QObject::connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
                     [&](int idx) {
        if (idx == 0) tm.apply(dark);
        else          tm.apply(light);
    });

    window.show();
    return app.exec();
}

编译

# CMakeLists.txt snippet
find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
add_executable(theme_demo
    minimal_theme.cpp
    minimal_theme_manager.cpp
    main.cpp
)
target_link_libraries(theme_demo Qt6::Core Qt6::Widgets)

这套代码可以直接编译,验证冷切换和 JSON 持久化。

8. 参考文件

qlementine 中的相关源文件:

文件 内容
Theme.hpp Theme 结构体完整字段定义(color + size + spacing token)
Theme.cpp JSON 序列化/反序列化实现
ThemeManager.hpp 单例管理器声明,themeChanged() 信号
ThemeManager.cpp load/save 逻辑 + 信号发射

建议在阅读后续文章前先浏览这些文件,建立完整的字段印象。


下一篇: Style 绘制框架与分层架构——在 Theme 数据层之上,我们将看到 QStyle 如何利用这些 token 构建统一的 draw 函数清单。