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 等第三方控件库,通常要区分两类主题:
- 控件库自己的主题
- 应用自己的业务主题
例如 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 重新解析到新的颜色和样式资源。