ThemeManager - 主题管理器¶
ThemeManager 是整个 UI 主题系统的入口,负责注册主题工厂、管理主题实例的生命周期,以及向已注册的 widget 分发主题变更事件。采用单例模式是因为整个应用程序只需要一个全局的主题管理点,多处实例化反而会造成状态同步的混乱。
获取单例¶
获取管理器实例的方式很直接,返回的是引用而不是指针——这样能避免空指针检查,同时也符合"全局唯一实例"的语义:
单例的初始化是线程安全的(C++11 魔法静态变量保证),不用担心多线程竞态。但我们只在 UI 线程里使用它,Qt 的信号机制本身就要求如此。
注册主题工厂¶
主题不是直接创建的,而是通过工厂模式。在使用前需要先注册工厂,名字是后续获取主题的唯一标识:
// 注册一个内置主题
manager.insert_one("default", []() {
return std::make_unique<DefaultThemeFactory>();
});
// 注册自定义主题
manager.insert_one("dark_blue", []() {
return std::make_unique<DarkBlueFactory>();
});
insert_one 返回 bool,如果名字已经存在会返回 false。这个设计挺实用,可以用来检测插件冲突——同一主题被两个插件注册时,至少有一个会失败。
工厂函数是延迟调用的,注册时只是把工厂创建器存起来,真正创建主题实例是在第一次访问时。这意味着你可以提前注册所有可用主题,而不用担心那些可能永远不会被用到的主题占用资源。
切换主题¶
切换主题只需要调用 setThemeTo,管理器会通知所有已注册的 widget:
// 切换到 dark 主题
manager.setThemeTo("dark_blue");
// 切换时可以选择不广播(特殊场景下使用)
manager.setThemeTo("default", false);
默认情况下,切换主题会广播 themeChanged 信号到所有通过 install_widget 注册的 widget。doBroadcast 参数设为 false 会跳过广播,这主要用于批量操作时避免重复通知——比如你要连续切换三个主题,中间的两次就不需要广播。
⚠️ 传入的名字必须已经通过 insert_one 注册过,否则行为是未定义的。我们当时考虑过抛异常,但 Qt 的信号槽机制和异常配合得不太好,所以选择了静默失败。如果你需要检测错误,建议在调用前先确认主题已注册。
Widget 注册与通知¶
widget 需要主动注册才能收到主题变更通知:
class MyWidget : public QWidget {
public:
MyWidget(QWidget* parent = nullptr) : QWidget(parent) {
// 注册到主题管理器
cf::ui::core::ThemeManager::instance().install_widget(this);
// 连接主题变更信号
connect(&cf::ui::core::ThemeManager::instance(),
&cf::ui::core::ThemeManager::themeChanged,
this, &MyWidget::onThemeChanged);
}
~MyWidget() {
// 记得取消注册
cf::ui::core::ThemeManager::instance().remove_widget(this);
}
private slots:
void onThemeChanged(const cf::ui::core::ICFTheme& new_theme) {
// 应用新主题
const auto& colors = new_theme.color_scheme();
setStyleSheet(QString("background-color: %1;")
.arg(colors.background().name()));
}
};
管理器持有的是 QWidget 的裸指针观察者,不负责生命周期。widget 析构时必须调用 remove_widget,否则管理器里会留下悬空指针。这个设计有点不安全,但 Qt 的对象树机制使得在 widget 析构时自动清理变得复杂,我们选择了简单粗暴的显式管理方式。
获取主题实例¶
有时候你不需要切换主题,只是想读取当前或某个主题的配置:
// 获取当前主题名称
const std::string& current = manager.currentThemeName();
// 获取特定主题的引用(延迟创建)
const cf::ui::core::ICFTheme& theme = manager.theme("default");
// 读取颜色配置
const auto& colors = theme.color_scheme();
QColor bg = colors.background();
theme() 方法返回的是常量引用,主题实例会被缓存起来,后续访问直接从缓存取。第一次访问时才会调用工厂创建,所以第一次可能会有轻微延迟。
移除主题¶
如果一个主题不再需要,可以动态移除:
移除操作不会影响当前正在使用的主题——即使你移除的是当前主题,管理器里仍然保留着它的实例,直到切换到另一个主题。这个行为可能是 feature 也可能是 bug,取决于你从哪个角度看,但它确实避免了一些诡妙的崩溃。
线程安全¶
管理器的内部状态(工厂注册、widget 集合、主题缓存)没有加锁保护。这是有意为之——我们假设所有操作都在 UI 线程上进行,而 Qt 的信号槽机制本身就要求如此。
如果你确实需要在其他线程里访问主题数据,可以:
1. 在后台线程里只读取主题配置(ICFTheme 本身是线程安全的)
2. 通过 QMetaObject::invokeMethod 把 UI 操作转发到主线程
⚠️ 不要在多线程环境下同时调用 insert_one、remove_one 或 setThemeTo,一定会翻车。
完整示例¶
这是一个完整的使用流程,展示了从注册到切换的完整链条:
// 1. 定义工厂(实际项目里通常在单独文件中)
class LightThemeFactory : public cf::ui::core::ThemeFactory {
public:
std::unique_ptr<cf::ui::core::ICFTheme> fromName(const char* name) override {
auto theme = std::make_unique<cf::ui::core::ICFTheme>();
theme->color_scheme_ = std::make_unique<LightColorScheme>();
theme->radius_scale_ = std::make_unique<DefaultRadiusScale>();
// ... 其他组件
return theme;
}
std::unique_ptr<cf::ui::core::ICFTheme> fromJson(const QByteArray& json) override {
// 解析 JSON 并创建主题
return nullptr;
}
QByteArray toJson(cf::ui::core::ICFTheme* raw_theme) override {
// 序列化主题到 JSON
return QByteArray();
}
};
// 2. 应用初始化时注册所有主题
void App::initThemes() {
auto& manager = cf::ui::core::ThemeManager::instance();
manager.insert_one("light", []() {
return std::make_unique<LightThemeFactory>();
});
manager.insert_one("dark", []() {
return std::make_unique<DarkThemeFactory>();
});
}
// 3. Widget 连接主题变更
void MyWidget::setupThemeConnection() {
auto& manager = cf::ui::core::ThemeManager::instance();
manager.install_widget(this);
connect(&manager, &cf::ui::core::ThemeManager::themeChanged,
this, &MyWidget::applyTheme);
// 立即应用当前主题
applyTheme(manager.theme(manager.currentThemeName()));
}
// 4. 用户切换主题时
void SettingsDialog::onThemeSelected(const QString& name) {
cf::ui::core::ThemeManager::instance().setThemeTo(name.toStdString());
}