Skip to content

WinUI 参考: 完整的属性清单与设计指南见 Style

Microsoft.UI.Reactor(Reactor)里的样式是一条分层的修饰符链。最底层是 WinUI 主题系统 —— 每一个可见的颜色最终都是一个画笔资源,在渲染时 针对当前生效的 Light / Dark / HighContrast 主题字典解析。 往上一层是 ThemeRef,一个按键名引用这些资源之一的 record-struct。 再往上是两个接口面:带类型的访问器,如 Theme.AccentTheme.AccentTextTheme.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.AccentTheme.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.CardStrokeBorder。把这些修饰符与布局容器组合起来,就得到可复用的卡片形态:

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.AccentTheme.AccentText……)是用规范键字符串构造同一个 ThemeRef record-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);
    }
}

配色方案 Hook

ColorScheme 有三个取值:LightDarkHighContrast。 当你需要让逻辑(而不只是颜色)基于主题分支时 —— 比如选用不同的图标资源、 选择不同的布局密度,或打不同的埋点标签 —— 就用这个 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

这些按钮流畅方法在 DropDownButtonSplitButtonToggleSplitButton 上也有重载。先考虑它们,再考虑 .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, 因此该区域内的卡片、按钮和文字都会采用深色方案。这个形态适合预览面板、 仅深色模式的视频播放器,或嵌在整体浅色应用中的营销版块。

常见错误

存在令牌的地方用了硬编码颜色字面量

// 别这样 —— 跨主题时是冻结的。
TextBlock("Subtitle").Foreground("#888888");

这个灰色在浅色模式下看着没问题,到了深色模式下就惨白一片。 正确的令牌是 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

// 别这样 —— 已经有对应的修饰符了。
Button("Save", onSave).Set(c => c.RequestedTheme = ElementTheme.Dark);

.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 逃生舱,以及捕获其误用的分析器规则