WinUI 参考: 完整的属性面与设计指导,参见 Colors。
Microsoft.UI.Reactor(Reactor)的主题令牌是给颜色起的一个名字,由系统在渲染
时解析。同一个令牌(Theme.Accent)在应用处于浅色模式、深色模式或高
对比度时会发出不同的 Brush —— 而且在用户翻转
主题时无需重渲染即可切换。另一种做法 —— 把 "#0066CC" 这样的字面量写死在
修饰符里 —— 会把值锁死在单一视觉模式上,用户一打开设置就静默
破坏深色主题。令牌存在的意义,是让
样式层能把画笔交给控件,而不必关心当前是哪个主题。最常见的错误是为了
「我就把这一次写死吧」而去用字面量;
REACTOR_THEME_001 分析器会在构建时捕获它,使它无法
交付出去。
主题令牌¶
Reactor 在静态的
Theme 类上暴露了 36 个具名主题令牌,另加一个 Theme.Ref(string) 逃生通道,
用于任意 XAML 资源键。令牌通过 WinUI 的资源
系统(XamlControlsResources + 应用的 ThemeDictionaries)解析,因此
它们产生的值与 Microsoft 在
WinUI Fluent 颜色中交付的设计系统一致。
引导片段¶
class SwatchGrid : Component
{
public override Element Render() => ScrollView(
VStack(16,
Heading("Theme tokens"),
SwatchSection("Accent", new[] {
("Accent", Theme.Accent),
("AccentSecondary", Theme.AccentSecondary),
("AccentTertiary", Theme.AccentTertiary),
("AccentDisabled", Theme.AccentDisabled),
}),
SwatchSection("Text", new[] {
("PrimaryText", Theme.PrimaryText),
("SecondaryText", Theme.SecondaryText),
("TertiaryText", Theme.TertiaryText),
("DisabledText", Theme.DisabledText),
("AccentText", Theme.AccentText),
}),
SwatchSection("Surfaces", new[] {
("SolidBackground", Theme.SolidBackground),
("CardBackground", Theme.CardBackground),
("SmokeFill", Theme.SmokeFill),
("SubtleFill", Theme.SubtleFill),
("LayerFill", Theme.LayerFill),
}),
SwatchSection("Control fill", new[] {
("ControlFill", Theme.ControlFill),
("ControlFillSecondary", Theme.ControlFillSecondary),
("ControlFillTertiary", Theme.ControlFillTertiary),
("ControlFillDisabled", Theme.ControlFillDisabled),
("ControlFillInputActive", Theme.ControlFillInputActive),
}),
SwatchSection("Stroke", new[] {
("CardStroke", Theme.CardStroke),
("SurfaceStroke", Theme.SurfaceStroke),
("DividerStroke", Theme.DividerStroke),
("ControlStroke", Theme.ControlStroke),
("ControlStrokeSecondary", Theme.ControlStrokeSecondary),
}),
SwatchSection("Signal", new[] {
("SystemAttention", Theme.SystemAttention),
("SystemSuccess", Theme.SystemSuccess),
("SystemCaution", Theme.SystemCaution),
("SystemCritical", Theme.SystemCritical),
("SystemNeutral", Theme.SystemNeutral),
("SystemSolidNeutral", Theme.SystemSolidNeutral),
("SystemSolidAttention", Theme.SystemSolidAttention),
("SystemAttentionBackground", Theme.SystemAttentionBackground),
("SystemSuccessBackground", Theme.SystemSuccessBackground),
("SystemCautionBackground", Theme.SystemCautionBackground),
("SystemCriticalBackground", Theme.SystemCriticalBackground),
("SystemNeutralBackground", Theme.SystemNeutralBackground),
})
).Padding(20)
);
private static Element SwatchSection(string title, (string Name, ThemeRef Ref)[] tokens) =>
VStack(8,
SubHeading(title),
VStack(4, tokens.Select(t => Row(t.Name, t.Ref).WithKey(t.Name)).ToArray())
);
private static Element Row(string name, ThemeRef token) => HStack(12,
new BorderElement(Empty())
.Background(token)
.Size(40, 24)
.WithBorder(Theme.ControlStroke),
TextBlock(name).Width(220),
TextBlock(token.ResourceKey).Opacity(0.6)
);
}

同一个网格,两种主题。每个格子都是 ThemeRef
针对当前生效的 ThemeDictionary 解析出的结果;切换主题是对宿主的
一次属性更新,而不是组件的重渲染。
注意: 在
.Background("#0066CC")里写死颜色字符串会彻底跳过 主题切换。用户翻到 深色模式时按钮依然是蓝色,而「仍然偏蓝的背景上的白字」在浅色的中性色之下 还能满足 WCAG —— 但它背后的深色模式中性色让页面看起来是坏的。 请在.Background/.Foreground/.WithBorder上使用Theme.Accent(或匹配的Theme.Ref(key)); REACTOR_THEME_001 会在构建时标记出 字面量,让这个回归永远到不了评审环节。
解析过程¶
ThemeRef 是一个只读 record struct,持有
资源键。当 .Background(token) 这样的修饰符到达
协调器时,框架会从已落实的
FrameworkElement 沿可视化树向上查找最接近的 RequestedTheme,回退到 ActualTheme,再
回退到 Application.RequestedTheme。所选主题名对应的画刷
来自 Application.Resources.ThemeDictionaries[name](或
最近的合并字典)。
class ThemeRefGood : Component
{
public override Element Render() =>
Button("Click me", () => { }).Background(Theme.Accent);
}
当用户翻转系统主题时,WinUI 会引发
ActualThemeChanged;Reactor 订阅它,并更新
所有引用了 ThemeRef 的元素上的画刷。需要按配色方案分支的组件
使用 UseColorScheme:
// UseColorScheme 以响应式方式读取当前配色方案(Light / Dark),
// 组件可以直接按该值分支,而不必重新实现解析器。
class SchemeAwareBadge : Component
{
public override Element Render()
{
var scheme = UseColorScheme();
var label = scheme == ColorScheme.Dark ? "Dark mode" : "Light mode";
return TextBlock(label).Foreground(Theme.PrimaryText);
}
}
UseColorScheme() 返回应用级的 ColorScheme(Light 或 Dark),并在
该值变化时重跑渲染。仅当分支无法用令牌表达时
(例如「渲染哪个图标」这类与颜色无关的决定)才动用它。它不报告强制
颜色模式 —— 那要用 UseHighContrast()。
令牌目录¶
6 组共 36 个具名令牌,另加 Theme.Ref(string) 逃生
通道。下面的列表与 src/Reactor/Core/Theme.cs 完全一致。
强调色(4 个令牌)¶
| 令牌 | WinUI 键 | 用于何处 |
|---|---|---|
Theme.Accent |
AccentFillColorDefaultBrush |
主要操作按钮的背景。 |
Theme.AccentSecondary |
AccentFillColorSecondaryBrush |
按下/悬停的强调色变体。 |
Theme.AccentTertiary |
AccentFillColorTertiaryBrush |
细微的强调色表面。 |
Theme.AccentDisabled |
AccentFillColorDisabledBrush |
强调色控件的禁用状态。 |
文本(5 个令牌)¶
| 令牌 | WinUI 键 | 用于何处 |
|---|---|---|
Theme.PrimaryText |
TextFillColorPrimaryBrush |
标题与正文。 |
Theme.SecondaryText |
TextFillColorSecondaryBrush |
辅助文案。 |
Theme.TertiaryText |
TextFillColorTertiaryBrush |
图注与时间戳。 |
Theme.DisabledText |
TextFillColorDisabledBrush |
禁用标签。 |
Theme.AccentText |
AccentTextFillColorPrimaryBrush |
行内强调文本(链接、徽章)。 |
表面(5 个令牌)¶
| 令牌 | WinUI 键 | 用于何处 |
|---|---|---|
Theme.SolidBackground |
SolidBackgroundFillColorBaseBrush |
页面背景。 |
Theme.CardBackground |
CardBackgroundFillColorDefaultBrush |
卡片/面板容器。 |
Theme.SmokeFill |
SmokeFillColorDefaultBrush |
模态遮罩。 |
Theme.SubtleFill |
SubtleFillColorSecondaryBrush |
列表行的悬停底色。 |
Theme.LayerFill |
LayerFillColorDefaultBrush |
堆叠卡片层。 |
控件填充(5 个令牌)¶
| 令牌 | WinUI 键 | 用于何处 |
|---|---|---|
Theme.ControlFill |
ControlFillColorDefaultBrush |
默认的中性控件表面。 |
Theme.ControlFillSecondary |
ControlFillColorSecondaryBrush |
悬停状态。 |
Theme.ControlFillTertiary |
ControlFillColorTertiaryBrush |
按下状态。 |
Theme.ControlFillDisabled |
ControlFillColorDisabledBrush |
禁用。 |
Theme.ControlFillInputActive |
ControlFillColorInputActiveBrush |
处于活动状态的 TextBox / NumberBox 填充。 |
描边(5 个令牌)¶
| 令牌 | WinUI 键 | 用于何处 |
|---|---|---|
Theme.CardStroke |
CardStrokeColorDefaultBrush |
卡片轮廓。 |
Theme.SurfaceStroke |
SurfaceStrokeColorDefaultBrush |
页面级分隔线。 |
Theme.DividerStroke |
DividerStrokeColorDefaultBrush |
行内列表分隔线。 |
Theme.ControlStroke |
ControlStrokeColorDefaultBrush |
默认控件边框。 |
Theme.ControlStrokeSecondary |
ControlStrokeColorSecondaryBrush |
焦点/按下时的边框。 |
信号色(12 个令牌)¶
| 令牌 | WinUI 键 | 用于何处 |
|---|---|---|
Theme.SystemAttention |
SystemFillColorAttentionBrush |
需要注意的提示。 |
Theme.SystemSuccess |
SystemFillColorSuccessBrush |
成功状态。 |
Theme.SystemCaution |
SystemFillColorCautionBrush |
警告状态。 |
Theme.SystemCritical |
SystemFillColorCriticalBrush |
错误状态。 |
Theme.SystemNeutral |
SystemFillColorNeutralBrush |
信息/中性。 |
Theme.SystemSolidNeutral |
SystemFillColorSolidNeutralBrush |
实心中性表面。 |
Theme.SystemSolidAttention |
SystemFillColorSolidAttentionBackgroundBrush |
实心注意表面。 |
Theme.SystemAttentionBackground |
SystemFillColorAttentionBackgroundBrush |
注意横幅的背景。 |
Theme.SystemSuccessBackground |
SystemFillColorSuccessBackgroundBrush |
成功横幅的背景。 |
Theme.SystemCautionBackground |
SystemFillColorCautionBackgroundBrush |
警告横幅的背景。 |
Theme.SystemCriticalBackground |
SystemFillColorCriticalBackgroundBrush |
错误横幅的背景。 |
Theme.SystemNeutralBackground |
SystemFillColorNeutralBackgroundBrush |
信息横幅的背景。 |
这就是完整集合:4 + 5 + 5 + 5 + 5 + 12 = 36 个有类型的令牌,
另加 Theme.Ref(key) 用于资源树中的其他任何东西。
自定义键¶
// 通过字符串键引用任意 XAML 资源 —— 覆盖应用级重写
// 以及 Reactor 没有以有类型访问器暴露的任何令牌。
Button("Custom", () => { })
.Background(Theme.Ref("MyAppTitleBarBackground"));
模式¶
按严重级别为主题化 InfoBar¶
信号背景令牌与前景信号令牌配对,
可以产出在两种主题下都正确可读的行内状态横幅。
判别式请使用 WinUI 的 InfoBarSeverity —— Reactor 自己的
Severity 枚举(Microsoft.UI.Reactor.Controls.Validation)是给
表单校验消息用的,成员也不同
(Info / Warning / Error)。
// 把一个严重级别映射到 Signal 令牌对上(前景 + 匹配的
// 背景),让横幅在浅色与深色下都跟随主题。
class StatusBannerDemo : Component
{
public override Element Render() => VStack(8,
StatusBanner("Saved", InfoBarSeverity.Success),
StatusBanner("Disk almost full", InfoBarSeverity.Warning),
StatusBanner("Upload failed", InfoBarSeverity.Error),
StatusBanner("Sync scheduled", InfoBarSeverity.Informational)
).Padding(16);
internal static Element StatusBanner(string text, InfoBarSeverity severity) => HStack(8,
TextBlock(text).Foreground(severity switch
{
InfoBarSeverity.Success => Theme.SystemSuccess,
InfoBarSeverity.Warning => Theme.SystemCaution,
InfoBarSeverity.Error => Theme.SystemCritical,
_ => Theme.SystemNeutral,
})
).Background(severity switch
{
InfoBarSeverity.Success => Theme.SystemSuccessBackground,
InfoBarSeverity.Warning => Theme.SystemCautionBackground,
InfoBarSeverity.Error => Theme.SystemCriticalBackground,
_ => Theme.SystemNeutralBackground,
}).Padding(12).CornerRadius(4);
}
把它与 命令 结合,就能在 Command<T> 成功/失败时
渲染同样的横幅。
应用级令牌重写¶
当设计系统需要一个不在 WinUI 调色板里的品牌色时,
在 Application.Resources(或一个合并字典)里定义一次,然后
通过 Theme.Ref 取用:
<!-- App.xaml -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="BrandPrimaryBrush" Color="#005FB8" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="BrandPrimaryBrush" Color="#60AAFA" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
这个品牌色能参与主题切换,是因为该重写具有
浅色与深色两个变体。不带 ThemeDictionaries 地定义它
会把它冻结在单一主题上 —— 那正是 Theme.Ref 被造出来
要防止的陷阱。
逐元素主题重写¶
单个子树可以通过
RequestedTheme 退出应用主题 —— 对于应当始终看起来是浅色的视频播放器表面或
打印预览窗格很有用:
Theme.Resolve 的向上查找会优先采纳局部重写,因此
该子树内的每个 ThemeRef 都会针对浅色
字典解析,即使应用其余部分是深色。
常见错误¶
硬编码颜色字面量¶
// Don't:
Button("Save", () => { }).Background("#0066CC");
// ^^^^^^^^^^
// REACTOR_THEME_001 — 主题感知修饰符上的硬编码颜色字面量。
// 用户翻到深色模式时该按钮仍然是蓝色。
class ThemeRefGood : Component
{
public override Element Render() =>
Button("Click me", () => { }).Background(Theme.Accent);
}
字面量把值锁死在一个主题上;令牌会参与 切换。分析器在构建时就把这一点暴露出来,让回归 无法交付;请在 CI 中把该警告当作错误。
只测浅色而漏掉深色回归¶
一个常见的评审失误:PR 里的截图是浅色模式,评审者 签发通过,然后深色模式的破坏就落到了 main 上。
陷阱在于想用一个无头测试来覆盖它。下面这个测试看起来 像是检查了两种主题,其实并不能:
[Theory]
[InlineData(ElementTheme.Light)]
[InlineData(ElementTheme.Dark)]
public void StatusBanner_renders_in_both_themes(ElementTheme theme)
{
var tree = StatusBanner("Saved", InfoBarSeverity.Success)
.RequestedTheme(theme);
// 空洞无力:扫描器检查的是无障碍元数据 —— 名称、
// 角色、标签 —— 从不解析画刷。两种情形返回
// 相同的结果,因此一个硬编码为仅浅色可读的横幅
// 与一个正确的横幅一样容易通过。`theme` 参数
// 根本没有到达这项断言所读取的任何东西。
Assert.Empty(AccessibilityScanner.Scan(tree));
}
颜色回归只有在意法树被落实、资源查找针对生效主题运行时 才可观察,因此它需要一个针对真实 WinUI 控件的 自测 —— 无头测试根本无法构造这些控件。请针对每种 主题断言解析后的画刷,或对渲染出的控件做快照;对元素 记录做断言只能证明你构建了你构建的那个元素。
把非主题化的键当作 ThemeRef 解析¶
对 Application.Resources 中才存在
(而不在任何 ThemeDictionaries 条目中)的资源使用
Theme.Ref("MyAppHeaderBackground") 时,无论主题如何都会返回同一个画刷。
页面能渲染,但颜色不会切换。如果目标是主题感知,该资源必须位于
ThemeDictionaries 中;否则请在应用代码里把颜色写死,
并用注释标记它是有意为之。
提示¶
先找有类型的令牌,其次才是 Theme.Ref 逃生通道。
有类型的令牌有 IntelliSense 与文档注释;字符串键没有。
如果某个颜色在有类型的面上缺失,而它是稳定的 WinUI 画刷,
那就把它加到 Theme.cs,而不是让 Theme.Ref("…") 调用
四处增生。
在 CI 中把 REACTOR_THEME_001 与 REACTOR_THEME_004 当作错误。 修一个 硬编码颜色的代价是一行代码;深色主题回归落到 main 上的 代价是一份客户报告。
不要浪费轮次在每次渲染时重新解析。 ThemeRef 是一个
record struct —— 栈分配。解析器每次应用修饰符时调用一次;
UseColorScheme 只在配色方案真正变化时才重跑渲染。
参考¶
| 概念 | API | 说明 |
|---|---|---|
| 令牌 | Theme.<Name> |
src/Reactor/Core/Theme.cs 中的 36 个有类型访问器。 |
| 逃生通道 | Theme.Ref(key) |
任意 XAML 资源键,包括应用级重写。 |
| 响应式读取 | UseColorScheme() |
返回 ColorScheme;切换时触发重渲染。 |
| 逐元素重写 | .RequestedTheme(ElementTheme) |
遍历可视化树,解析最近的重写。 |
| 分析器 | REACTOR_THEME_001、REACTOR_THEME_004 |
标记主题感知修饰符上的硬编码字面量与 Brush/Color 对象。 |
| WinUI 面 | XamlControlsResources |
持有每个具名令牌的 ThemeDictionary。 |
后续阅读¶
- 样式 —— 样式链中的上一篇:
消费
ThemeRef值的修饰符面。 - 动画 —— 下一篇:在主题化值之间做动画; 画刷如何在主题切换中过渡。
- 无障碍 —— 决定每个表面该取用哪个令牌的 对比度预算。
- DevTools 内部机制 —— 开发菜单里的 主题切换开关如何驱动本页所记录的同一个解析器。
- Reactor 的规则 —— REACTOR_THEME_001 分析器 与其他被强制执行的惯用法并列出现的地方。