WinUI 参考: 完整的属性清单与设计指南见 Style。
Microsoft.UI.Reactor(Reactor)里的样式是一条分层的修饰符链。最底层是
WinUI 主题系统 —— 每一个可见的颜色最终都是一个画笔资源,在渲染时
针对当前生效的 Light / Dark / HighContrast 主题字典解析。
往上一层是 ThemeRef,一个按键名引用这些资源之一的 record-struct。
再往上是两个接口面:带类型的访问器,如 Theme.Accent、Theme.AccentText、
Theme.CardBackground(规范、可自动补全、重构安全),以及
Theme.Ref("AnyResourceKey"),用于 Reactor 尚未以属性形式暴露的那些
长尾 WinUI 画笔。令牌之上是修饰符快捷方式 —— .AccentButton()、
.SubtleButton()、以及按信号严重级别分类的流畅方法 ——
它们把一个小的命名样式选择烘焙进一次调用里。再之上是通过
.Resources(r => r.Set(key, value)) 实现的轻量样式化,它为某个子树覆盖
个别 WinUI 资源键,而不替换控件模板。整条栈默认就是主题感知的:
令牌、修饰符快捷方式和 Resource 覆盖都会在生效主题切换时重新解析。
逃生舱则是字面量十六进制字符串("#7B61FF")和直接的
.Set(control => ...) 属性写入 —— 两者从构造上就是主题冻结的,
只有在品牌色需要无视模式保持恒定时,它们才是正确的工具。
样式与主题¶
WinUI 参考: 关于底层画笔资源、Fluent 设计语言以及完整的样式接口面, 参见 Microsoft Learn 上的 Design Windows apps overview。
速查表¶
| API | 层级 | 何时使用 |
|---|---|---|
Theme.Accent、Theme.PrimaryText 等 |
带类型的令牌 | 总是用它 —— 这些是标准的画笔引用。 |
Theme.Ref("AnyResourceKey") |
字符串令牌 | 该画笔没有对应的带类型访问器(罕见)。 |
.Background(token) / .Foreground(token) |
颜色修饰符 | 把令牌应用到暴露该属性的元素上(例如 Border 用背景、TextBlock 用前景)。 |
.Background("#RRGGBB") |
颜色修饰符 | 必须跨主题保持恒定的品牌色。 |
.AccentButton()、.SubtleButton()、.TextLink() |
命名样式流畅方法 | 标准的四种按钮形态。无需 .Resources。 |
.Informational() / .Success() / .Warning() / .Error() |
命名样式流畅方法 | InfoBar 的严重级别。 |
.Resources(r => r.Set(key, value)) |
轻量样式化 | 为子树覆盖个别 WinUI 资源键。 |
.RequestedTheme(ElementTheme.Dark) |
区域根 | 把一棵子树钉在某个特定的配色方案上。 |
UseColorScheme() / UseIsDarkTheme() |
Hook | 让逻辑(而不只是颜色)基于应用全局主题分支。 |
.Set(control => control.Foreground = ...) |
逃生舱 | 控件上有某个属性,令牌和 .Resources 都够不到。 |
主题令牌¶
用 .Background() 或 .Foreground() 应用主题令牌:
class ThemeTokensExample : Component
{
public override Element Render()
{
return VStack(12,
TextBlock("Primary Text").Foreground(Theme.PrimaryText),
TextBlock("Secondary Text").Foreground(Theme.SecondaryText),
TextBlock("Accent Text").Foreground(Theme.AccentText).SemiBold(),
Border(
TextBlock("On Accent Background")
.Foreground(Theme.Ref("TextOnAccentFillColorPrimaryBrush"))
.Padding(horizontal: 8, vertical: 4)
).Background(Theme.Accent)
.CornerRadius(4)
).Padding(24);
}
}

每个令牌都会在渲染时解析为一个 WinUI 画笔。当用户在浅色与深色模式之间
切换时,每一个绑定到 ThemeRef 的元素都会自动更新 —— 无需手动重新绑定。
完整令牌目录(含浅色/深色色板)见 theming-tokens。
构建卡片¶
卡片就是一个带 Theme.CardBackground、圆角、内边距和 Theme.CardStroke 的
Border。把这些修饰符与布局容器组合起来,就得到可复用的卡片形态:
class CardLayoutExample : Component
{
public override Element Render()
{
return VStack(16,
Heading("Dashboard"),
HStack(12,
Card("Users", "1,204", Theme.Accent),
Card("Revenue", "$48.2k", Theme.SystemSuccess),
Card("Errors", "3", Theme.SystemCritical)
)
).Padding(24);
}
static Element Card(string title, string value, ThemeRef accent) =>
Border(
VStack(8,
Caption(title).Foreground(Theme.SecondaryText),
TextBlock(value).FontSize(28).Bold().Foreground(accent)
).Padding(16)
).Background(Theme.CardBackground)
.CornerRadius(8)
.WithBorder(Theme.CardStroke, 1)
.Width(160);
}

Theme.CardBackground 是标准的 WinUI 卡片表面;Theme.CardStroke 是与之
配套的边框。两者结合产出的卡片,在浅色和深色模式下都显得原生。
颜色修饰符¶
.Background() 和 .Foreground() 既接受带类型的令牌,也接受 Theme.Ref
令牌,以获得主题感知的颜色:
class ColorModifiersExample : Component
{
public override Element Render()
{
return VStack(8,
Border(TextBlock("Typed token").Padding(8))
.Background(Theme.SubtleFill),
Border(TextBlock("Theme.Ref token").Padding(8))
.Background(Theme.Ref("AcrylicBackgroundFillColorDefaultBrush")),
Border(TextBlock("Token foreground + token background")
.Foreground(Theme.PrimaryText)
.Padding(8))
.Background(Theme.ControlFill)
).Padding(24);
}
}
| 重载 | 示例 | 主题感知? |
|---|---|---|
| 带类型的令牌 | .Background(Theme.Accent) |
是 |
Theme.Ref |
.Background(Theme.Ref("MyCustomBrush")) |
是 |
| 十六进制字符串 | .Background("#FF5733") |
否 —— 冻结;留给品牌色 |
Brush |
.Background(new SolidColorBrush(...)) |
否 —— 冻结;除非颜色确实不该跟随主题,否则避免使用 |
令牌是正确的默认选择。十六进制和 Brush 留给那些无论模式如何都应保持不变的
品牌色。
信号色¶
状态指示器使用语义化的信号色:
class SignalColorsExample : Component
{
public override Element Render()
{
return HStack(12,
Badge("Info", Theme.SystemAttention),
Badge("Success", Theme.SystemSuccess),
Badge("Warning", Theme.SystemCaution),
Badge("Error", Theme.SystemCritical)
).Padding(24);
}
static Element Badge(string label, ThemeRef color) =>
Border(
TextBlock(label)
.FontSize(12).SemiBold()
.Foreground(color)
.Padding(horizontal: 8, vertical: 4)
).Background(Theme.SubtleFill)
.CornerRadius(4);
}

用它们代替硬编码的红/绿/黄 —— 它们在两种主题下都满足
无障碍对比度要求,并且在 HighContrast 下色调转换正确。
注意:
Theme.Ref("SomeResourceKey")每次渲染都要通过字符串查找来解析, 遍历合并后的资源字典链去找那个画笔;而带类型的访问器(Theme.Accent、Theme.AccentText……)是用规范键字符串构造同一个ThemeRefrecord-struct。 带类型的访问器路径之所以是受支持的那一条,有两个理由:(1) 重命名一个 WinUI 资源时,带类型访问器会编译报错,而Theme.Ref只会在运行时拿到一个 空画笔 —— 后者会静默回退到系统默认值;(2)REACTOR_THEME_001分析器只在 十六进制值与某个已知令牌值匹配时,才把.Background(...)里的十六进制字符串 标记为警告,因此当Foo作为带类型访问器并不存在时,.Background(Theme.Ref("Foo"))对分析器是不可见的。规则是: 只要存在带类型的Theme.X,就用它。Theme.Ref(...)留给 Reactor 尚未暴露的 长尾画笔,而且与其在代码库里留一个字符串引用,不如提个特性请求把访问器加上。
深色与浅色模式¶
.RequestedTheme(ElementTheme.Dark) 把一棵子树钉在特定主题上。
UseColorScheme() 和 UseIsDarkTheme() 让你以响应式的方式读取生效主题:
class DarkLightToggleExample : Component
{
public override Element Render()
{
var (isDark, setIsDark) = UseState(false);
var theme = isDark ? ElementTheme.Dark : ElementTheme.Light;
return VStack(16,
ToggleSwitch(isDark, setIsDark, onContent: "Dark", offContent: "Light"),
Border(
VStack(12,
TextBlock("This panel follows the toggle.").Foreground(Theme.PrimaryText),
TextBlock("Background adapts automatically.").Foreground(Theme.SecondaryText)
).Padding(16)
).Background(Theme.CardBackground)
.CornerRadius(8)
.RequestedTheme(theme)
).Padding(24);
}
}

.RequestedTheme(...) 在底层 FrameworkElement 上设置该属性。
后代中所有 ThemeRef 绑定都会针对新方案重新解析。UseIsDarkTheme()
在生效方案为深色时返回 true —— 当你需要不同的行为(不同的图标、
不同的布局)而不只是不同的颜色时,就基于它做分支。
响应式主题 Hook¶
UseColorScheme() 返回应用全局的配色方案;它不会观测逐元素的
RequestedTheme 覆盖。UseIsDarkTheme() 是一个便捷包装,
把它收窄为一个布尔值:
class ColorSchemeHookExample : Component
{
public override Element Render()
{
var isDark = UseIsDarkTheme();
var scheme = UseColorScheme();
return VStack(12,
TextBlock($"Color scheme: {scheme}").FontSize(16).SemiBold(),
TextBlock(isDark ? "Dark mode is active" : "Light mode is active")
.Foreground(Theme.SecondaryText),
Border(
TextBlock(isDark ? "Dark content" : "Light content")
.Padding(12)
).Background(Theme.CardBackground)
.CornerRadius(8)
.WithBorder(Theme.CardStroke, 1)
).Padding(24);
}
}

ColorScheme 有三个取值:Light、Dark 和 HighContrast。
当你需要让逻辑(而不只是颜色)基于主题分支时 —— 比如选用不同的图标资源、
选择不同的布局密度,或打不同的埋点标签 —— 就用这个 Hook。
命名样式流畅方法¶
Reactor 把标准 WinUI 命名样式暴露为流畅快捷方式。常见场景从来不需要
.Resources(...) 块:
class NamedStylesExample : Component
{
public override Element Render()
{
return VStack(12,
HStack(8,
Button("Save", () => { }).AccentButton(),
Button("Cancel", () => { }).SubtleButton(),
HyperlinkButton("Learn more", new Uri("https://example.com"))
.TextLink()
),
InfoBar(title: "Heads up", message: "Backups run nightly.")
.Informational(),
InfoBar(title: "Saved", message: "All changes are persisted.")
.Success(),
InfoBar(title: "Almost full", message: "75% of quota used.")
.Warning(),
InfoBar(title: "Sync failed", message: "Check your network.")
.Error()
).Padding(24);
}
}
| 流畅方法 | 映射到 |
|---|---|
.AccentButton() |
AccentButtonStyle —— 填充强调色 |
.SubtleButton() |
SubtleButtonStyle —— 悬停前仅显示文字 |
.TextLink() |
超链接样式的按钮(无边框装饰) |
.Informational() |
InfoBarSeverity.Informational |
.Success() |
InfoBarSeverity.Success |
.Warning() |
InfoBarSeverity.Warning |
.Error() |
InfoBarSeverity.Error |
这些按钮流畅方法在 DropDownButton、SplitButton 和 ToggleSplitButton
上也有重载。先考虑它们,再考虑 .Resources(...) —— 它们省去了
悬停/按下/禁用六个键的繁琐组合,并且与系统主题更新保持同步。
清单见 spec 039 §2.1、§2.2 与 §2.4。
轻量样式化¶
.Resources(...) 覆盖 WinUI 控件的资源键,而不替换控件模板。
VisualStateManager 的各个状态(悬停、按下、禁用)会自动遵循你的覆盖:
class LightweightStylingExample : Component
{
public override Element Render()
{
return VStack(12,
Button("Default Button", () => { }),
Button("Accent Button", () => { })
.Resources(r => r
.Set("ButtonBackground", Theme.Accent)
.Set("ButtonBackgroundPointerOver", Theme.AccentSecondary)
.Set("ButtonBackgroundPressed", Theme.AccentTertiary)
.Set("ButtonForeground", "#FFFFFF")
.Set("ButtonForegroundPointerOver", "#FFFFFF")
.Set("ButtonForegroundPressed", "#FFFFFF")),
Button("Danger Button", () => { })
.Resources(r => r
.Set("ButtonBackground", Theme.SystemCritical)
.Set("ButtonBackgroundPointerOver", "#C42B1C")
.Set("ButtonForeground", "#FFFFFF")
.Set("ButtonForegroundPointerOver", "#FFFFFF"))
).Padding(24);
}
}

ResourceBuilder 支持 .Set(key, ThemeRef) 做主题响应式的覆盖、
.Set(key, string) 设置十六进制颜色、.Set(key, double) 设置数值、
.Set(key, CornerRadius) 设置圆角半径。覆盖会通过 WinUI 的资源字典
层级向下级联到子元素。
自定义资源访问¶
对于 Reactor 尚未以带类型属性暴露的画笔,Theme.Ref() 接受 WinUI 资源键:
class CustomResourceExample : Component
{
public override Element Render()
{
return VStack(12,
TextBlock("Using a named WinUI resource:")
.Foreground(Theme.PrimaryText),
TextBlock("NavigationViewItemForeground")
.Foreground(Theme.Ref("NavigationViewItemForeground"))
).Padding(24);
}
}
该键必须存在于 WinUI 的资源字典中。为什么存在带类型访问器时应优先使用它, 见上面的注意事项。
模式¶
随深浅色自适应的界面¶
大多数界面通过令牌就能自动适配 —— 同一份代码在两种主题下都能跑,
因为每个颜色都是 ThemeRef。剩下 5% 需要分支:选用不同的字形、
切换到更暗的英雄图、播放不同的背景动画。这正是 UseIsDarkTheme()
的用武之地 —— 基于这个 Hook 分支,返回不同的元素。完整令牌目录与色板
参考见 theming-tokens。
在应用根节点覆盖品牌色¶
在应用根节点放一个 .Resources(...) 块,就能给所有后代的强调色换肤。
这个覆盖会流到每一个解析 AccentFillColorDefaultBrush 的后代 ——
带 .AccentButton() 的按钮、用 Theme.Accent 的文字,以及任何引用了该
画笔的地方:
// 在应用根节点覆盖品牌色 —— 每个解析 AccentFillColorDefaultBrush
// 的后代,都会在两种主题下取用这个品牌色。
class BrandOverrideExample : Component
{
public override Element Render()
{
return VStack(12,
SubHeading("Brand color cascades through descendants"),
Button("Save", () => { }).AccentButton(),
TextBlock("Accented text").Foreground(Theme.AccentText).SemiBold()
).Padding(24)
// 根节点上一处 Resources 覆盖,就能给所有后代换肤。
// 跨主题:如果浅色与深色应取不同的品牌色调,
// 就两个 ThemeDictionaries 都设。
.Resources(r => r
.Set("AccentFillColorDefaultBrush", "#7B61FF")
.Set("AccentTextFillColorPrimaryBrush", "#7B61FF"));
}
}
要获得完整的品牌支持,请在浅色和深色两个主题字典里都设置覆盖 ——
两个取值可能需要是不同的色调才能保持清晰可读。如果品牌色对比度与系统配对
不同,还可以配合覆盖 Theme.AccentText。
逐元素的主题覆盖作用域¶
把某一个面板钉在另一种配色方案上,而应用其余部分仍跟随系统。关键在于把
.RequestedTheme(...) 应用到区域根 —— 也就是那个其子元素应当针对该
覆盖进行解析的容器 —— 而不是某个叶子元素:
// 逐元素的主题覆盖作用域 —— 在一个整体为 Light 的应用里,
// 把单个面板强制为 Dark,无需应用级的 RequestedTheme。
class ScopedThemeOverrideExample : Component
{
public override Element Render()
{
return VStack(16,
SubHeading("Default scheme"),
Border(VStack(8,
TextBlock("Default scheme — follows the app theme.")
.Foreground(Theme.PrimaryText),
TextBlock("Card stroke and background also follow.")
.Foreground(Theme.SecondaryText)
).Padding(16)).Background(Theme.CardBackground)
.WithBorder(Theme.CardStroke, 1).CornerRadius(8),
SubHeading("Dark scope — bound to a region root"),
Border(VStack(8,
TextBlock("This subtree is always dark.")
.Foreground(Theme.PrimaryText),
TextBlock("ThemeRef descendants resolve against the override.")
.Foreground(Theme.SecondaryText)
).Padding(16)).Background(Theme.CardBackground)
.WithBorder(Theme.CardStroke, 1).CornerRadius(8)
// 由区域根承载这个覆盖 —— 在叶子节点上覆盖
// 正是「常见错误」一节里点名的反面模式。
.RequestedTheme(ElementTheme.Dark)
).Padding(24);
}
}
被覆盖子树中的 ThemeRef 解析会向上走到最近的显式 RequestedTheme,
因此该区域内的卡片、按钮和文字都会采用深色方案。这个形态适合预览面板、
仅深色模式的视频播放器,或嵌在整体浅色应用中的营销版块。
常见错误¶
存在令牌的地方用了硬编码颜色字面量¶
这个灰色在浅色模式下看着没问题,到了深色模式下就惨白一片。
正确的令牌是 Theme.SecondaryText —— 这个画笔在两种主题下都会解析成
合适的灰色。REACTOR_THEME_001 分析器会把与某个已知令牌解析值匹配的
十六进制字面量标记出来,并提供代码修复,改写成带类型的访问器。
存在带类型访问器时却用了 Theme.Ref¶
// 别这样 —— 今天能跑,但对重命名很脆弱。
.Background(Theme.Ref("AccentFillColorDefaultBrush"))
// 应该这样 —— 重构安全且可被发现。
.Background(Theme.Accent)
带类型访问器产出的 ThemeRef record-struct 与字符串查找完全相同 ——
在渲染时两者毫无区别。差别在编写期:一个被重命名或移除的 WinUI 资源,
对带类型访问器是编译期失败,对 Theme.Ref 则是静默返回 null(系统默认值)。
完整理由见上面的注意事项。
把 RequestedTheme 设置在叶子元素上¶
// 别这样 —— 叶子的主题覆盖不会级联,因为根本没有后代可以接收它。
// 按钮自身的画笔会解析,但它周围的面板和任何同级控件都不会。
Border(VStack(8,
Button("Click").RequestedTheme(ElementTheme.Dark), // 叶子上的覆盖
TextBlock("Caption") // 不受影响
))
RequestedTheme 是一个沿可视化树向下流动的 FrameworkElement 属性。
请把它应用到区域根 —— 也就是"其后代应当针对该覆盖解析"的最高层元素 ——
而不是某个单独的叶子。正确的形态就是上面的逐元素作用域模式。
为主题相关的属性去用 .Set¶
.RequestedTheme(...) 修饰符会参与属性差异比对 —— 下一次移除该修饰符的
渲染会把属性走回默认值。而 .Set 是命令式的、单向的,下一次渲染不会撤销它。
REACTOR_THEME_003 分析器会捕获这一具体情况,并提供代码修复改成修饰符。
关于 .Set 的更广泛讨论见 advanced。
Roslyn 分析器¶
| 分析器 | 严重级别 | 检查内容 |
|---|---|---|
REACTOR_THEME_001 |
警告 | 应使用 ThemeRef,而不是硬编码的颜色字符串。 |
REACTOR_THEME_002 |
提示 | 视觉状态覆盖可考虑使用轻量样式化。 |
REACTOR_THEME_003 |
提示 | 存在可用的 RequestedTheme 修饰符。 |
REACTOR_THEME_004 |
警告 | 硬编码的 Brush/Color 对象绕过了主题令牌。 |
每个分析器都附带一个代码修复,可转换为推荐写法。通过引用
Reactor.Analyzers 项目即可启用 —— 用 mur new 创建的项目默认开启。
小贴士¶
优先用带类型的令牌,而不是 Theme.Ref(string)。 带类型访问器
有编译期检查、重构安全,而且在自动补全里能被发现。Theme.Ref
留给 Reactor 尚未暴露的极少数画笔。
把 .RequestedTheme() 用在区域根上,而不是叶子上。 覆盖沿可视化树
向下流动,所以叶子上的覆盖到不了同级元素。正确的形态是每个作用域一个
RequestedTheme,放在该作用域的容器上。
控件颜色覆盖请用 .Resources()。 轻量样式化会保留悬停/按下/禁用状态。
.Set 只强制一个值,会破坏这些状态。
条件逻辑请用 UseIsDarkTheme()。 当你需要基于主题做出不同的
行为(不同的图标、不同的布局)时,读这个 Hook,而不是手动检查资源。
两种主题都要测。 跑起应用,把 Windows 切到深色模式,确认没有变得 不可读的内容。只要避免硬编码颜色,令牌系统就能处理好这件事 —— 分析器会兜住那些明显的疏漏。
下一步¶
- Theming tokens —— 下一篇:完整令牌目录,含浅色/深色色板
- Layout —— VStack、HStack、Grid 等你正在美化的那些表面的核心容器
- Flex Layout —— 用于自适应对齐与换行的 flex 容器
- Accessibility —— 确保主题化颜色在两种方案下都满足 WCAG 对比度
- Animation —— 把主题感知的画笔与过渡、
.InteractionStates(...)搭配使用 - Components —— 把带样式的元素组合成可复用的函数组件或类组件
- Advanced ——
.Set逃生舱,以及捕获其误用的分析器规则