主题样式与自定义控件
本文介绍 Watt Toolkit(SteamTools)Avalonia 客户端的主题系统与自定义控件样式体系:AppTheme 主题模型、App.Theme.cs 中的主题切换实现、CustomTheme 自定义样式类,以及 UI/Styling 目录下的 XAML 样式资源(控件、字体、图标、窗口等)。该体系负责应用级浅色/深色/高对比度/跟随系统主题的解析与切换,以及强调色(AccentColor)的定制。
Purpose and Scope
本页覆盖以下内容:
AppTheme枚举与IApplication.Theme契约(主题的抽象定义)。- Avalonia
App分部类中Theme属性的完整切换控制流,包括"跟随系统"与固定主题之间的双向迁移逻辑。 SetThemeNotChangeValue如何把AppTheme映射为 AvaloniaThemeVariant(含自定义的HighContrastTheme变体),并同步 LiveCharts 图表主题。CustomTheme自定义样式类(Styles+IResourceProvider)的结构与UI/Styling下的 XAML 资源文件。SetThemeAccent强调色设置与FluentAvaloniaTheme的集成。- 平台主题桥接:
IPlatformService的IsLightOrDarkTheme与SetLightOrDarkThemeFollowingSystem。
不属于本页范围、由兄弟页面承接的内容:
- 各平台(Windows/Linux/macOS)主题 API 的具体实现细节属于
IPlatformService.Theme平台服务页(对应src/BD.WTTS.Client/Services.Implementation/Platform/*/*PlatformServiceImpl.Theme.cs)。 - DI 注册细节见
ServiceCollectionExtensions.TryAddAvaloniaThemeService.cs所在的服务注册主题页。 - 图表(LiveCharts)本身的绘制逻辑不在本页展开,本页仅说明其与主题的联动点。
Overview
主题系统的目标是在跨平台(Windows、Linux、macOS、移动端)的 Avalonia UI 上提供一致且可跟随系统外观切换的视觉样式,核心需求包括:
- 三种用户可选主题 + 跟随系统:浅色(Light)、深色(Dark)、跟随系统(FollowingSystem),另外保留高对比度(HighContrast)这一特殊变体,由
AppTheme枚举表达(见src/BD.WTTS.Client/Enums/AppTheme.cs)。 - 运行时切换无重启:通过设置 Avalonia 的
RequestedThemeVariant触发资源字典(Themes.axaml中按变体组织的资源)整体刷新,无需重建窗口。 - 第三方渲染组件同步:应用内嵌 LiveCharts 图表(如加速统计页),必须在主题切换时同步调用
LiveCharts.Configure(AddLightTheme/AddDarkTheme),否则图表配色与背景脱节。 - 平台级跟随系统:由
IPlatformService暴露IsLightOrDarkTheme(当前系统是否浅色,可为null表示未知)与SetLightOrDarkThemeFollowingSystem(bool)(打开/关闭跟随系统),平台实现在WindowsPlatformServiceImpl.Theme.cs、LinuxPlatformServiceImpl.Theme.cs、MacCatalystPlatformServiceImpl.Theme.cs中。 - 自定义控件样式集中管理:
src/BD.WTTS.Client.Avalonia/UI/Styling/目录下的Controls.axaml、Fonts.axaml、Icons.axaml、InfoBox.axaml、Themes.axaml、Window.axaml承载全局控件模板、字体、图标与窗口样式,CustomTheme.axaml(.cs)是其宿主样式类。
关键术语:
AppTheme:平台无关的主题枚举(业务层语义)。ThemeVariant:Avalonia 原生主题变体(Light/Dark/Default),驱动资源查找。RequestedThemeVariant:Application级属性,应用请求的主题变体,赋值后全局资源重新解析。FluentAvaloniaTheme:FluentAvalonia 库的主题对象,提供CustomAccentColor/PreferUserAccentColor强调色定制。
Architecture
主题体系按职责分为四层:契约层(IApplication.Theme)、实现层(Avalonia App 分部类)、样式资源层(CustomTheme + UI/Styling XAML)、平台桥接层(IPlatformService.Theme 及各平台实现)。
分层设计意图:BD.WTTS.Client(可被非 UI 项目引用的核心层)只定义 AppTheme 语义与 IApplication.Theme 契约,不依赖 Avalonia;Avalonia 宿主项目通过分部类 App 实现切换细节;平台差异(注册表、GTK、macOS 外观 API)被隔离在 IPlatformService 各平台实现中。这样业务代码(如设置页 ViewModel)只需读写 Ioc.Get<IApplication>().Theme,无需感知渲染框架与操作系统。
Main Content
主题契约:IApplication.Theme 与 AppTheme
IApplication 以分部接口形式拆分主题相关成员(src/BD.WTTS.Client/App/IApplication.Theme.cs),为所有宿主(Avalonia 桌面、移动端等)提供统一访问点:
AppTheme Theme { get; set; }:当前设置的主题(含"跟随系统"这个元状态)。AppTheme ActualTheme:只读派生属性,把"跟随系统"折叠为实际生效的浅色/深色。AppTheme DefaultActualTheme:各宿主定义的默认值(Avalonia 宿主中为AppTheme.FollowingSystem)。static GetAppThemeByIsLightOrDarkTheme(bool):布尔到主题的内联小工具。GetActualThemeByFollowingSystem():读取IPlatformService.Instance.IsLightOrDarkTheme;当平台返回null(无法探测)时回退到DefaultActualTheme。
ActualTheme 的表达式主体与 GetActualThemeByFollowingSystem 的空值回退是理解整条链路的关键——平台可能无法给出系统主题,因此契约层必须显式定义回退行为。
1AppTheme ActualTheme => Theme switch
2{
3 AppTheme.FollowingSystem => GetActualThemeByFollowingSystem(),
4 AppTheme.Light => AppTheme.Light,
5 AppTheme.Dark => AppTheme.Dark,
6 _ => DefaultActualTheme,
7};1protected AppTheme GetActualThemeByFollowingSystem()
2{
3 var dps = IPlatformService.Instance;
4 var isLightOrDarkTheme = dps.IsLightOrDarkTheme;
5 if (isLightOrDarkTheme.HasValue)
6 {
7 return GetAppThemeByIsLightOrDarkTheme(isLightOrDarkTheme.Value);
8 }
9 return DefaultActualTheme;
10}主题切换控制流:App.Theme.cs
App.Theme.cs 是 Avalonia 宿主对上述契约的实现(分部类 App,即 Application 派生类)。默认值常量为 AppTheme.FollowingSystem,与应用首次启动即跟随系统外观的预期一致。
Theme setter 是整个体系最核心的状态迁移函数,需要处理四种状态组合:(旧值, 新值) ×(是否为"跟随系统")。实现要点:
- 去重短路:
if (value == mTheme) return;避免重复触发资源重解析。 - 切入"跟随系统"(新值 = FollowingSystem):读取平台当前系统主题并换算为具体变体,调用
SetLightOrDarkThemeFollowingSystem(true)打开平台级跟随,若换算结果与当前实际主题相同则仅更新记录值(goto setValue),不触发全局样式刷新。 - 切离"跟随系统"(旧值 = FollowingSystem,新值为固定主题):先
SetLightOrDarkThemeFollowingSystem(false)关闭跟随,再判断系统主题换算值是否与新值一致,一致则同样短路。 - 普通切换:调用
SetThemeNotChangeValue(switch_value)应用实际样式,最后统一在标签setValue:处提交mTheme = value。
1public AppTheme Theme
2{
3 get => mTheme;
4 set
5 {
6 if (value == mTheme) return;
7 AppTheme switch_value = value;
8
9 if (value == AppTheme.FollowingSystem)
10 {
11 var dps = IPlatformService.Instance;
12 var isLightOrDarkTheme = dps.IsLightOrDarkTheme;
13 if (isLightOrDarkTheme.HasValue)
14 {
15 switch_value = IApplication.GetAppThemeByIsLightOrDarkTheme(isLightOrDarkTheme.Value);
16 dps.SetLightOrDarkThemeFollowingSystem(true);
17 if (switch_value == mTheme) goto setValue;
18 }
19 }
20 else if (mTheme == AppTheme.FollowingSystem)
21 {
22 var dps = IPlatformService.Instance;
23 dps.SetLightOrDarkThemeFollowingSystem(false);
24 var isLightOrDarkTheme = dps.IsLightOrDarkTheme;
25 if (isLightOrDarkTheme.HasValue)
26 {
27 var mThemeFS = IApplication.GetAppThemeByIsLightOrDarkTheme(isLightOrDarkTheme.Value);
28 if (mThemeFS == switch_value) goto setValue;
29 }
30 }
31
32 SetThemeNotChangeValue(switch_value);
33
34 setValue: mTheme = value;
35 }
36}设计意图:把"跟随系统"建模为 AppTheme 的一个元状态而非布尔开关,使得设置页只需一个下拉枚举即可表达全部语义;goto setValue 的短路优化则避免在视觉结果不变时(例如系统本来就是深色,用户从"深色"切到"跟随系统")触发一次昂贵的全应用资源重解析与图表重配置。
应用主题变体:SetThemeNotChangeValue
SetThemeNotChangeValue(AppTheme) 将业务枚举映射为 Avalonia ThemeVariant 并写入 RequestedThemeVariant;同时为 LiveCharts 配置匹配的主题,保证图表配色与应用主题一致。命名中的 "NotChangeValue" 表明它只应用样式、不修改 Theme 属性记录值,因此可安全地被系统主题变更回调复用(跟随系统时系统切换 → 直接调用本方法应用新变体,而 mTheme 保持 FollowingSystem 不变)。
1public void SetThemeNotChangeValue(AppTheme value)
2{
3 var mode = value switch
4 {
5 AppTheme.HighContrast => CustomTheme.HighContrastTheme,
6 AppTheme.Light => ThemeVariant.Light,
7 _ => ThemeVariant.Dark,
8 };
9
10 RequestedThemeVariant = mode;
11
12 if (value == AppTheme.Light)
13 {
14 LiveCharts.Configure(settings => settings.AddLightTheme());
15 }
16 else
17 {
18 LiveCharts.Configure(settings => settings.AddDarkTheme());
19 }
20}注意映射细节:_ => ThemeVariant.Dark 是兜底分支,意味着深色也是除 Light/HighContrast 之外所有值的缺省呈现——这与深色作为默认视觉基调的产品取向一致。
强调色定制:SetThemeAccent
SetThemeAccent(string? colorHex) 从应用样式中查找 FluentAvaloniaTheme 实例并定制强调色:
- 参数为合法十六进制颜色(
Color.TryParse成功)→CustomAccentColor = color,PreferUserAccentColor = false(使用应用自定义强调色)。 - 参数为字符串
"True"(bool.TrueString,大小写不敏感)→ 清空CustomAccentColor并PreferUserAccentColor = true,回退为跟随操作系统/用户强调色。 colorHex为null或找不到FluentAvaloniaTheme(例如宿主未加载该样式)时直接返回,静默容错。
1public static void SetThemeAccent(string? colorHex)
2{
3 if (colorHex == null)
4 {
5 return;
6 }
7
8 var thm = App.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
9
10 if (thm == null)
11 {
12 return;
13 }
14
15 if (Color.TryParse(colorHex, out var color))
16 {
17 thm.CustomAccentColor = color;
18 thm.PreferUserAccentColor = false;
19 }
20 else if (colorHex.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase))
21 {
22 thm.CustomAccentColor = null;
23 thm.PreferUserAccentColor = true;
24 }
25}实现中保留了被注释掉的按 Windows 版本选择 PreferUserAccentColor 的历史代码(OperatingSystem.IsWindowsVersionAtLeast(6, 2) 分支),说明早期版本曾针对 Windows 8 以下系统禁用用户强调色,后续简化为"True 字符串 = 跟随用户"。方法被声明为 static 且直接遍历 App.Current.Styles,而非通过 Ioc.Get<FluentAvaloniaTheme>()(该写法同样以注释形式保留),可避免对 DI 容器中该对象生命周期的依赖。
CustomTheme 自定义样式宿主
CustomTheme(src/BD.WTTS.Client.Avalonia/UI/Styling/CustomTheme.axaml.cs)是一个派生自 Styles 并实现 IResourceProvider 的自定义样式类,由 CustomTheme.axaml 提供资源内容。它定义了三个模式常量与一个自定义高对比度变体:
LightModeString = "Light"、DarkModeString = "Dark"、HighContrastModeString = "HighContrast"。HighContrastTheme = new ThemeVariant("HighContrast", ThemeVariant.Light)——以 Light 作为回退基变体注册的自定义变体,资源解析失败时可退回浅色资源,保证高对比度模式下未定义资源的可用性。ModeProperty(StyledProperty<string>,默认"Dark")+Mode属性:允许在 XAML 上以特性语法选择模式。- 两个构造函数(
Uri baseUri与IServiceProvider serviceProvider,后者从IUriContext取 BaseUri)都汇入Init(),通过AvaloniaXamlLoader.Load(this)加载 XAML 并置_hasLoaded = true。 GetThemeFromIPlatformSettings()把Mode字符串映射到ThemeVariant;ResolveThemeAndInitializeSystemResources()原本用于把系统主题写入Application.Current.RequestedThemeVariant,但其调用在Init()中被显式注释掉——主题初始化职责已上移到App层,CustomTheme退化为纯样式/资源载体。IResourceNode.HasResources显式返回true,声明该节点参与资源查找链。
1public class CustomTheme : Styles, IResourceProvider
2{
3 public const string LightModeString = "Light";
4 public const string DarkModeString = "Dark";
5 public const string HighContrastModeString = "HighContrast";
6
7 public static readonly ThemeVariant HighContrastTheme = new ThemeVariant(HighContrastModeString,
8 ThemeVariant.Light);
9
10 public static readonly StyledProperty<string> ModeProperty =
11 AvaloniaProperty.Register<CustomTheme, string>(nameof(Mode), DarkModeString);
12}1private ThemeVariant GetThemeFromIPlatformSettings()
2{
3 return Mode switch
4 {
5 LightModeString => ThemeVariant.Light,
6 DarkModeString => ThemeVariant.Dark,
7 HighContrastModeString => HighContrastTheme,
8 _ => ThemeVariant.Default,
9 };
10}Styling 目录:XAML 样式资源组织
src/BD.WTTS.Client.Avalonia/UI/Styling/ 下的文件各司其职(本页依据目录清单说明职责划分,具体资源键值见各文件):
| 文件 | 职责 |
|---|---|
CustomTheme.axaml / .axaml.cs | 自定义样式宿主,加载主题相关资源(见上一节) |
Themes.axaml | 主题资源字典,按 ThemeVariant(Light/Dark/HighContrast)组织颜色、画刷等资源 |
Controls.axaml | 全局控件样式/模板(按钮、列表、输入等基础控件定制) |
Fonts.axaml | 字体资源(FontFamily 定义与字体回退链) |
Icons.axaml | 图标资源(图标字体/路径数据,供页面按 key 引用) |
Window.axaml | 窗口级样式(标题栏、窗口边框等自定义窗口外观) |
InfoBox.axaml | 提示信息框(InfoBox)控件样式 |
配套的 DI 注册入口位于 src/BD.WTTS.Client.Avalonia/Extensions/ServiceCollection/ServiceCollectionExtensions.TryAddAvaloniaThemeService.cs,把 Avalonia 主题服务接入应用服务集合(注册细节属于服务注册主题页)。
Core Flow
以"用户在设置页把主题从深色切换为跟随系统"为例的时序(方法名与判断条件均取自 App.Theme.cs):
反向流程(从跟随系统切换到固定主题)走 mTheme == AppTheme.FollowingSystem 分支:先关闭平台跟随,再比对换算值,必要时应用新变体。系统侧主题变化事件(平台层监听系统外观变化)最终也落到 SetThemeNotChangeValue,从而保证"跟随系统"时 mTheme 记录值不变、实际变体实时刷新。
Configuration Options
| 选项/资源 | 类型 | 默认值 | 说明 |
|---|---|---|---|
IApplication.Theme | AppTheme | AppTheme.FollowingSystem | 用户选择的主题(元状态),由 _DefaultActualTheme 常量定义默认值 |
IApplication.DefaultActualTheme | AppTheme | AppTheme.FollowingSystem(Avalonia 宿主) | 实际主题无法探测时的回退值 |
RequestedThemeVariant | ThemeVariant? | 继承系统/Default | Avalonia 应用级请求变体,由 SetThemeNotChangeValue 写入 |
CustomTheme.Mode | string(StyledProperty) | "Dark"(DarkModeString) | XAML 上选择的模式字符串 |
CustomTheme.HighContrastTheme | ThemeVariant | "HighContrast"(基变体 Light) | 高对比度自定义变体定义 |
SetThemeAccent 参数 | string? | null(直接返回) | 合法颜色十六进制 = 自定义强调色;"True" = 跟随用户强调色 |
FluentAvaloniaTheme.CustomAccentColor | Color? | null | 自定义强调色(色值无效时保持不变) |
FluentAvaloniaTheme.PreferUserAccentColor | bool | — | 是否优先使用系统/用户强调色,与 CustomAccentColor 互斥使用 |
IPlatformService.IsLightOrDarkTheme | bool? | 平台相关 | 当前系统是否浅色;null 表示平台无法探测 |
IPlatformService.SetLightOrDarkThemeFollowingSystem(bool) | void | — | 打开/关闭平台级"跟随系统"同步 |
API Reference
App.Theme(IApplication.Theme 实现)
属性 setter,处理主题状态迁移。参数:value (AppTheme) 新主题。行为:去重短路 → "跟随系统"分支或"离开跟随系统"分支 → SetThemeNotChangeValue(switch_value) → mTheme = value。副作用:写 RequestedThemeVariant、配置 LiveCharts、调用平台 SetLightOrDarkThemeFollowingSystem。
void SetThemeNotChangeValue(AppTheme value)
应用主题变体但不修改 Theme 记录值。参数:value (AppTheme) 要呈现的具体主题(不应为 FollowingSystem)。行为:HighContrast → CustomTheme.HighContrastTheme、Light → ThemeVariant.Light、其余 → ThemeVariant.Dark;随后按 Light/非 Light 配置 LiveCharts 浅色/深色主题。调用方:Theme setter、系统主题变更回调路径。
static void SetThemeAccent(string? colorHex)
静态方法,设置强调色。参数:colorHex (string?)。分支:null 或无 FluentAvaloniaTheme → 返回;合法色值 → CustomAccentColor = color, PreferUserAccentColor = false;"True" → CustomAccentColor = null, PreferUserAccentColor = true;其他非法字符串 → 无操作。
IApplication.ActualTheme(只读派生属性)
FollowingSystem → GetActualThemeByFollowingSystem()(平台返回 null 时回退 DefaultActualTheme);Light/Dark 原样返回;其余 → DefaultActualTheme。
CustomTheme 构造与成员
CustomTheme(Uri baseUri)/CustomTheme(IServiceProvider serviceProvider):均调用Init()加载 XAML。string Mode { get; set; }:由ModeProperty支持的模式选择,取值"Light"/"Dark"/"HighContrast"。static ThemeVariant HighContrastTheme:预注册的高对比度变体。bool IResourceNode.HasResources => true:参与资源查找。
Failure Modes, Edge Cases & Concurrency
- 平台无法探测系统主题:
IsLightOrDarkTheme == null时,切入"跟随系统"分支只更新mTheme、不刷新样式(保持当前外观),且不会调用SetLightOrDarkThemeFollowingSystem;ActualTheme则回退DefaultActualTheme。契约层与实现层对null均有显式处理。 - 无效主题值兜底:
SetThemeNotChangeValue的 switch 兜底为ThemeVariant.Dark;GetThemeFromIPlatformSettings的兜底为ThemeVariant.Default(走系统/上级解析)。非法/新增枚举值不会抛异常,而是落到安全默认。 - 强调色输入容错:
SetThemeAccent对null、找不到FluentAvaloniaTheme、非法色值字符串均静默返回;仅"True"(不区分大小写)触发"跟随用户"重置。调用方需自行保证色值合法性,否则表现为"无效果"而非异常。 - 重复赋值短路:
Themesetter 首行if (value == mTheme) return;,避免设置页反复绑定写入造成资源重解析风暴;"跟随系统"与固定主题间的等价切换(视觉结果不变)也通过goto setValue短路。 - 线程模型:
Theme/SetThemeNotChangeValue直接操作Application.RequestedThemeVariant与Styles集合,属 UI 线程操作(AvaloniaStyledElement依赖属性要求 UI 线程访问);SetThemeAccent为static且访问App.Current?.Styles,同样应在 UI 线程调用。 - 主题记录值与实际变体解耦:
mTheme保存用户意图(可能为FollowingSystem),RequestedThemeVariant保存实际呈现。任何绕过 setter 直接调用SetThemeNotChangeValue的路径(如系统变更回调)不会污染用户意图,这是"跟随系统"在系统切换时仍保持开启的前提。
Performance & Operational Notes
- 资源重解析成本:
RequestedThemeVariant赋值触发 Avalonia 全局资源重新解析与控件样式重应用,是最昂贵的单步操作,因此实现中存在两处短路(值相等、等价视觉切换)以避免无谓刷新。 - LiveCharts 全局重配置:每次变体切换都会调用一次
LiveCharts.Configure(...),用于刷新已渲染图表的默认色板;这是主题切换时图表正确配色的保障,也是切换成本的一部分。 - 平台跟随开启是一次性调用:
SetLightOrDarkThemeFollowingSystem(true/false)只在进入/离开"跟随系统"状态时调用,不在每次变体应用时重复调用,减少对系统设置的写入。 - 运维角度:排查"主题不生效"问题时,检查顺序建议为
Ioc.Get<IApplication>().Theme(用户意图)→Application.RequestedThemeVariant(实际变体)→Themes.axaml中对应变体的资源键 →FluentAvaloniaTheme是否存在于App.Current.Styles(影响强调色)。
Extension Points
- 新增主题变体:仿照
HighContrastTheme的方式,在CustomTheme中注册new ThemeVariant("<name>", <基变体>),并在Themes.axaml中按ThemeDictionaries提供对应资源,再在SetThemeNotChangeValue的 switch 中加入映射分支即可,无需改动契约层。 - 扩展强调色来源:
SetThemeAccent目前接受十六进制字符串或"True",可在其解析分支中扩展更多语义值(如"System");底层CustomAccentColor/PreferUserAccentColor的组合即完整状态机。 - 新平台桥接:实现
IPlatformService的IsLightOrDarkTheme与SetLightOrDarkThemeFollowingSystem(参考WindowsPlatformServiceImpl.Theme.cs、LinuxPlatformServiceImpl.Theme.cs、MacCatalystPlatformServiceImpl.Theme.cs),App.Theme.cs的控制流无需任何修改。 - 样式资源扩展:新控件的全局样式建议继续集中在
Controls.axaml;字体、图标分别扩展Fonts.axaml、Icons.axaml,保持单一资源入口。
Related Links
- App.Theme.cs — 主题切换实现(本页核心源码)
- CustomTheme.axaml.cs — 自定义样式宿主类
- IApplication.Theme.cs — 主题契约
- IPlatformService.Theme.cs — 平台主题桥接契约
- WindowsPlatformServiceImpl.Theme.cs / LinuxPlatformServiceImpl.Theme.cs / MacCatalystPlatformServiceImpl.Theme.cs — 平台实现(详情见平台服务页)
- ServiceCollectionExtensions.TryAddAvaloniaThemeService.cs — 主题服务 DI 注册
- Styling XAML:Themes.axaml、Controls.axaml、Fonts.axaml、Icons.axaml、Window.axaml、InfoBox.axaml