田敏
返回博客列表
WPF

WPF 动态主题切换

田敏
2026-06-292 分钟阅读
WPF

WPF 动态主题切换常用方案笔记

在 WPF 中实现 Light / Dark 主题切换,核心思路通常不是去逐个修改控件颜色,而是把颜色、Brush、Style 抽到 ResourceDictionary 中,然后在运行时替换对应的资源字典。

一、核心概念

WPF 的样式资源通常放在 ResourceDictionary 中,例如:

Themes/
  AppLight.xaml
  AppDark.xaml
  CommonStyles.xaml

其中:

  • AppLight.xaml:浅色主题色板
  • AppDark.xaml:深色主题色板
  • CommonStyles.xaml:通用控件样式,例如 Button、TextBlock、DataGrid 等

主题切换时,通常只替换 AppLight.xaml / AppDark.xaml,公共样式不需要反复替换。

二、StaticResource 和 DynamicResource 的区别

如果资源需要在运行时切换,页面里应该使用 DynamicResource

<Grid Background="{DynamicResource AppBackgroundBrush}">
    <TextBlock
        Foreground="{DynamicResource TextBrush}"
        Text="Hello WPF" />
</Grid>

不要写成:

<Grid Background="{StaticResource AppBackgroundBrush}" />

StaticResource 更适合加载后不再变化的资源。它通常在 XAML 加载时解析,主题切换后不一定刷新。

DynamicResource 会在资源发生变化时重新查找资源,因此更适合动态主题。

三、主题资源文件示例

浅色主题:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <SolidColorBrush x:Key="AppBackgroundBrush" Color="#F3F6FA" />
    <SolidColorBrush x:Key="SurfaceBrush" Color="#FFFFFF" />
    <SolidColorBrush x:Key="TextBrush" Color="#111827" />
    <SolidColorBrush x:Key="TextMutedBrush" Color="#6B7280" />
    <SolidColorBrush x:Key="BorderBrushSoft" Color="#D8DEE8" />
    <SolidColorBrush x:Key="PrimaryBrush" Color="#2563EB" />

</ResourceDictionary>

深色主题:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <SolidColorBrush x:Key="AppBackgroundBrush" Color="#111827" />
    <SolidColorBrush x:Key="SurfaceBrush" Color="#1F2937" />
    <SolidColorBrush x:Key="TextBrush" Color="#E5E7EB" />
    <SolidColorBrush x:Key="TextMutedBrush" Color="#9CA3AF" />
    <SolidColorBrush x:Key="BorderBrushSoft" Color="#374151" />
    <SolidColorBrush x:Key="PrimaryBrush" Color="#60A5FA" />

</ResourceDictionary>

两个主题文件中的 x:Key 要保持一致。这样页面只依赖资源名,不关心当前是浅色还是深色。

四、App.xaml 中默认加载主题

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="/Themes/AppLight.xaml" />
            <ResourceDictionary Source="/Themes/CommonStyles.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

通常应用启动时先加载默认主题,例如 Light。

五、运行时切换主题

基本实现思路:

public static class ThemeManager
{
    public static void ApplyTheme(string themePath)
    {
        var dictionaries = Application.Current.Resources.MergedDictionaries;

        var oldTheme = dictionaries.FirstOrDefault(x =>
            x.Source?.OriginalString.Contains("/Themes/App") == true);

        if (oldTheme != null)
        {
            dictionaries.Remove(oldTheme);
        }

        dictionaries.Add(new ResourceDictionary
        {
            Source = new Uri(themePath, UriKind.Relative)
        });
    }
}

调用:

ThemeManager.ApplyTheme("/Themes/AppDark.xaml");
ThemeManager.ApplyTheme("/Themes/AppLight.xaml");

更完整一点,可以封装成枚举:

public enum AppTheme
{
    Light,
    Dark
}

然后统一由 ThemeService 管理:

public sealed class ThemeService
{
    public AppTheme CurrentTheme { get; private set; } = AppTheme.Light;

    public void Toggle()
    {
        Apply(CurrentTheme == AppTheme.Light ? AppTheme.Dark : AppTheme.Light);
    }

    public void Apply(AppTheme theme)
    {
        var path = theme == AppTheme.Dark
            ? "/Themes/AppDark.xaml"
            : "/Themes/AppLight.xaml";

        ThemeManager.ApplyTheme(path);
        CurrentTheme = theme;
    }
}

这样页面里只需要调用:

_themeService.Toggle();

六、结合第三方控件库

如果项目使用 HandyControl、MahApps、MaterialDesignThemes 等第三方控件库,通常要区分两类主题:

  1. 控件库自己的主题
  2. 应用自己的业务主题

例如 HandyControl 可以使用它自己的皮肤资源:

ResourceHelper.GetSkin(SkinType.Default);
ResourceHelper.GetSkin(SkinType.Dark);
ResourceHelper.GetTheme();

推荐做法是:

第三方控件库主题
+
自定义 AppLight / AppDark 业务色板
+
DynamicResource
+
统一 ThemeService

不要把所有业务颜色都绑死在控件库内部资源上。控件库负责基础控件风格,应用主题负责业务界面的背景、文字、边框、状态色等。

七、常见坑

1. 页面中使用了 StaticResource

如果写成:

Background="{StaticResource SurfaceBrush}"

主题切换后可能不会刷新。

应该改成:

Background="{DynamicResource SurfaceBrush}"

2. XAML 中写死颜色

例如:

<Border Background="#FFFFFF" />

这种颜色不会随着主题变化。应该提取成资源:

<Border Background="{DynamicResource SurfaceBrush}" />

3. ControlTemplate 中也要使用 DynamicResource

很多按钮、表格、输入框样式会写在 ControlTemplate 中。模板里的颜色同样需要动态资源:

<Setter TargetName="Root"
        Property="Background"
        Value="{DynamicResource PrimaryHoverBrush}" />

4. 资源字典顺序问题

MergedDictionaries 中资源可能会互相覆盖。通常建议:

控件库基础主题
控件库通用主题
应用主题色板
应用公共样式

具体顺序要根据项目实际资源依赖来调整。

5. 不要到处写切换逻辑

主题切换应该集中在一个服务中,例如 ThemeService。页面不应该直接操作 Application.Current.Resources.MergedDictionaries

这样后续扩展会更方便,例如:

  • 保存用户主题偏好
  • 跟随系统主题
  • 增加更多主题色
  • 启动时恢复上次主题

八、推荐实践总结

WPF 动态主题切换推荐方案:

1. 把颜色和 Brush 抽到 AppLight.xaml / AppDark.xaml
2. 页面和样式中使用 DynamicResource
3. 运行时替换 ResourceDictionary
4. 第三方控件库主题和应用业务主题分开管理
5. 用 ThemeService 统一封装切换逻辑

一句话总结:

WPF 主题切换的本质,是通过替换 ResourceDictionary,让 DynamicResource 重新解析到新的颜色和样式资源。

版权协议:MIT返回列表