关于 maui-theming
为 .NET MAUI 应用开发者提供主题设置指导,涵盖系统浅色/深色模式和自定义主题,并说明运行时切换、系统主题响应及偏好保存。
主要功能
- 01配置系统浅色和深色主题
- 02创建自定义 ResourceDictionary 主题
- 03运行时切换主题
- 04检测系统主题变化
- 05保存并恢复主题偏好
适用场景
- 01为 .NET MAUI 应用添加浅色和深色模式
- 02提供品牌主题选择
- 03响应操作系统主题变化
- 04在应用启动间保留主题设置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill maui-theming安装后在 Claude Code 中输入/maui-theming确认已加载。
附带文件references/theming-api.md
详细介绍
.NET MAUI 主题设置
在 .NET MAUI 应用中使用 AppThemeBinding、ResourceDictionary 交换和系统主题检测 API,实现浅色/深色模式支持、自定义品牌主题和运行时主题切换。
适用场景
- 为 .NET MAUI 应用添加浅色和深色模式支持
- 使用 ResourceDictionary 创建自定义品牌主题
- 在运行时检测并响应系统主题变化
- 允许用户选择偏好的主题(浅色、深色或系统默认)
- 将操作系统驱动的主题响应与自定义调色板结合使用
不适用场景
- 本地化或语言切换——请参阅 .NET MAUI 本地化文档
- 无障碍相关的视觉调整——请参阅 .NET MAUI 无障碍文档
- 应用图标或启动屏幕配置——请参阅 .NET MAUI 应用图标文档
- Bootstrap 风格的类主题设置——请参阅
Plugin.Maui.BootstrapThemeNuGet 包
输入
- 目标为 .NET 8 或更高版本的 .NET MAUI 项目
- 需要支持主题样式的 XAML 页面或 C# UI 代码
工作流
- 检测项目当前采用的主题方案(AppThemeBinding、ResourceDictionary 或未使用主题方案)。
- 选择合适的策略:简单的浅色/深色主题使用 AppThemeBinding,自定义/多主题使用 ResourceDictionary 交换,也可结合使用两者。
- 定义主题资源——内联的
AppThemeBinding值,或包含匹配键的独立ResourceDictionary文件。 - 在所有 XAML 页面中,将硬编码颜色替换为
DynamicResource绑定(或AppThemeBinding标记扩展)。 - 使用
Application.Current.RequestedTheme和RequestedThemeChanged事件检测系统主题。 - 使用
Preferences.Set/Preferences.Get持久化用户偏好,并在启动时应用。 - 确认 Android 的
MainActivity上设置了ConfigChanges.UiMode,以避免主题变化时重启 Activity。 - 至少在一个目标平台上测试浅色和深色主题,确认所有 UI 元素都能正确响应。
会改变答案的规则
请根据用户的场景检查以下规则,并且仅应用会影响其所问内容的规则。UiMode 和字典交换与运行时主题切换相关;如果问题只是询问如何设置 AppThemeBinding,这些内容就无关紧要。
回答要聚焦,但内容完整。 完整意味着展示实现所推荐方案的代码,而不是添加相邻主题的内容。如果推荐使用 DynamicResource,就展示让它生效的字典交换。如果用户询问如何在 C# 中设置浅色/深色颜色,就展示 SetAppThemeColor(颜色)和通用的 SetAppTheme<T>(任意可绑定属性类型),并优先使用资源键,而不是零散的硬编码颜色。不要附加用户没有询问的平台配置。
| 规则 | 应做 | 不应做 | 原因 |
|---|---|---|---|
| 运行时交换的值必须是动态的 | {DynamicResource Key} | {StaticResource Key} | StaticResource 在加载时解析一次,字典交换时不会更新。 |
Android 必须声明 UiMode (仅限运行时/系统主题切换) | 在 MainActivity 的 ConfigurationChanges 列表中包含 ConfigChanges.UiMode | 省略该项 | 否则,主题变化时 Android 会重启 Activity,导致导航状态丢失,看起来像应用崩溃。对于静态的 AppThemeBinding 设置,此项无关。 |
通过 UserAppTheme 强制设置主题 | Application.Current.UserAppTheme = AppTheme.Dark | 手动重新分配颜色 | UserAppTheme 会覆盖操作系统设置;AppTheme.Unspecified 会恢复为跟随系统主题。 |
不要将可用的 AppThemeBinding 设置替换为 ResourceDictionary 交换(反之亦然),除非用户确实需要另一种方案提供的能力——超过两种主题,或允许用户选择主题。
选择方案
| 方案 | 适用场景 | 限制 |
|---|---|---|
| AppThemeBinding | 随操作系统自动切换浅色/深色主题——代码最少 | 只有两种主题(浅色 + 深色) |
| ResourceDictionary swap | 自定义品牌主题、多于两种主题、用户偏好 | 设置较复杂;必须在所有地方使用 DynamicResource |
| 两者结合 | 响应操作系统变化,同时使用自定义主题颜色 | 灵活性最高,但也最复杂 |
AppThemeBinding(操作系统浅色/深色主题)
AppThemeBinding 会根据当前系统主题选择值。它支持 Light、Dark,以及可选的 Default 回退值。
只定义一次调色板——不要到处散布字面值
在每个元素上都写 {AppThemeBinding Light=#333333, Dark=#FFFFFF} 是最常见的主题设置错误:调色板会在数十个文件中重复,无法集中修改。建议将以下结构作为最终方案,而不是使用内联字面值:
<Application.Resources>
<ResourceDictionary>
<Color x:Key="LightPageBackground">#FFFFFF</Color>
<Color x:Key="DarkPageBackground">#1E1E1E</Color>
<Color x:Key="LightPrimaryText">#333333</Color>
<Color x:Key="DarkPrimaryText">#E0E0E0</Color>
<Style TargetType="ContentPage" ApplyToDerivedTypes="True">
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource LightPageBackground},
Dark={StaticResource DarkPageBackground}}" />
</Style>
<Style TargetType="Label">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource LightPrimaryText},
Dark={StaticResource DarkPrimaryText}}" />
</Style>
</ResourceDictionary>
</Application.Resources>
页面随后无需任何主题标记——它们会隐式应用这些样式。只有确实需要单独处理的情况才使用内联 AppThemeBinding,即便如此,也应引用 {StaticResource} 键,而不是直接使用十六进制字面值。
XAML(内联形式,适用于单独处理的情况)
<Label Text="Themed text"
TextColor="{AppThemeBinding Light=Green, Dark=Red}"
BackgroundColor="{AppThemeBinding Light=White, Dark=Black}" />
<Label TextColor="{AppThemeBinding Light={StaticResource LightPrimary},
Dark={StaticResource DarkPrimary}}" />
C# 扩展方法
回答有关“C# 中的浅色/深色颜色”的问题时,应同时展示两种方法——SetAppThemeColor
适用于 Color 属性,SetAppTheme<T> 适用于其他所有情况:
var label = new Label();
// Color-specific helper
label.SetAppThemeColor(Label.TextColorProperty, Colors.Green, Colors.Red);
// Generic helper — works for any bindable property type, not just Color
label.SetAppTheme<Color>(Label.TextColorProperty, Colors.Green, Colors.Red);
label.SetAppTheme<double>(Label.FontSizeProperty, 14, 16);
// The BindableProperty must belong to the object you call it on —
// Image.SourceProperty goes on an Image, not a Label.
var image = new Image();
image.SetAppTheme<ImageSource>(Image.SourceProperty,
ImageSource.FromFile("logo_light.png"),
ImageSource.FromFile("logo_dark.png"));
最好将值定义为资源键并引用它们,而不是在整个代码库中散布硬编码的颜色。
ResourceDictionary 主题设置(自定义主题)
使用键名相同的独立 ResourceDictionary 文件定义主题,然后在运行时切换。
步骤 1 — 定义主题字典
使用带有 x:Class 的编译 XAML(如下所示)时,每个字典都需要一个调用 InitializeComponent() 的代码隐藏文件。通过 Source 加载且不带 x:Class 的字典则不需要代码隐藏文件。
LightTheme.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Themes.LightTheme">
<Color x:Key="PageBackgroundColor">White</Color>
<Color x:Key="PrimaryTextColor">#333333</Color>
<Color x:Key="AccentColor">#2196F3</Color>
</ResourceDictionary>
LightTheme.xaml.cs
namespace MyApp.Themes;
public partial class LightTheme : ResourceDictionary
{
public LightTheme() => InitializeComponent();
}
创建对应的 DarkTheme.xaml / DarkTheme.xaml.cs,使用相同的键并设置不同的值。
步骤 2 — 使用 DynamicResource
使用 DynamicResource,以便在运行时切换字典时更新值:
<ContentPage BackgroundColor="{DynamicResource PageBackgroundColor}">
<Label Text="Hello"
TextColor="{DynamicResource PrimaryTextColor}" />
<Button Text="Action"
BackgroundColor="{DynamicResource AccentColor}" />
</ContentPage>
步骤 3 — 在运行时切换主题
🚨 切勿调用
MergedDictionaries.Clear()来切换主题。 默认的 MAUI 模板会将Resources/Styles/Colors.xaml和Styles.xaml合并到Application.Resources中。调用Clear()会连它们也一起移除,导致应用中的所有隐式样式、 画刷和颜色都悄然消失——按钮、输入框和标签都会恢复为未设置样式的默认状态。已验证:调用Clear()后,MergedDictionaries的数量会从 2 降到 1,模板中的Primary颜色也无法再解析。
只移除你添加的主题,其他内容保持不变:
static ResourceDictionary? _currentTheme;
void ApplyTheme(ResourceDictionary theme)
{
var merged = Application.Current!.Resources.MergedDictionaries;
// ✅ Remove ONLY the previous theme — Colors.xaml / Styles.xaml survive
if (_currentTheme is not null)
merged.Remove(_currentTheme);
merged.Add(theme);
_currentTheme = theme;
}
// Usage
ApplyTheme(new DarkTheme());
// ❌ Destroys the app's Colors.xaml and Styles.xaml along with the old theme
var merged = Application.Current!.Resources.MergedDictionaries;
merged.Clear();
merged.Add(theme);
检测系统主题
读取当前主题
AppTheme currentTheme = Application.Current!.RequestedTheme;
// Returns AppTheme.Light, AppTheme.Dark, or AppTheme.Unspecified
覆盖系统主题
// Force dark mode regardless of OS setting
Application.Current!.UserAppTheme = AppTheme.Dark;
// Reset to follow system theme
Application.Current!.UserAppTheme = AppTheme.Unspecified;
响应主题变化
Application.Current!.RequestedThemeChanged += (s, e) =>
{
AppTheme newTheme = e.RequestedTheme;
// Update UI or switch ResourceDictionaries
};
结合两种方案
使用带有 DynamicResource 值的 AppThemeBinding,以获得最大的灵活性——嵌套的 DynamicResource 会保持动态,因此切换字典时会更新值,同时仍会遵循操作系统的浅色/深色切换:
<Label TextColor="{AppThemeBinding
Light={DynamicResource LightPrimary},
Dark={DynamicResource DarkPrimary}}" />
或者响应系统变化并切换整个字典:
Application.Current!.RequestedThemeChanged += (s, e) =>
{
ApplyTheme(e.RequestedTheme == AppTheme.Dark
? new DarkTheme()
: new LightTheme());
};
保存并恢复用户偏好
使用 Preferences 保存用户的选择,并在启动时应用:
// Save choice
Preferences.Set("AppTheme", "Dark");
// Restore on startup (in App constructor or CreateWindow)
var saved = Preferences.Get("AppTheme", "System");
Application.Current!.UserAppTheme = saved switch
{
"Light" => AppTheme.Light,
"Dark" => AppTheme.Dark,
_ => AppTheme.Unspecified
};
常见问题
Android:必须设置 ConfigChanges.UiMode
MainActivity 必须包含 ConfigChanges.UiMode,否则主题变化事件不会触发,Activity 会重启,而不是平稳地处理变化:
[Activity(Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
ConfigurationChanges = ConfigChanges.ScreenSize
| ConfigChanges.Orientation
| ConfigChanges.UiMode // ← Required for theme detection
| ConfigChanges.ScreenLayout
| ConfigChanges.SmallestScreenSize
| ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity { }
如果没有 UiMode,在 Android 设置中切换深色模式会导致 Activity 完全重启——导航状态丢失,而且看起来像是应用崩溃。声明该设置后,应用会保持运行,并触发 RequestedThemeChanged,因此应将此修复与重新应用主题的处理程序配合使用(见下文)。
DynamicResource 与 StaticResource
使用 ResourceDictionary 切换主题时,必须使用 DynamicResource:
<Label TextColor="{DynamicResource PrimaryTextColor}" />
<Label TextColor="{StaticResource PrimaryTextColor}" />
只有在实际替换字典时,DynamicResource 才能发挥作用。诊断此问题时,务必同时展示字典替换和系统主题钩子,以及修复方法——否则用户虽然修正了绑定,但主题仍然不会更新:
static ResourceDictionary? _currentTheme;
void ApplyTheme(bool useDark)
{
var merged = Application.Current!.Resources.MergedDictionaries;
// Remove only the previous theme — never Clear(), which also wipes
// the template's Colors.xaml / Styles.xaml
if (_currentTheme is not null)
merged.Remove(_currentTheme);
_currentTheme = useDark ? new DarkTheme() : new LightTheme();
merged.Add(_currentTheme);
}
// React to the OS switching light/dark
Application.Current!.RequestedThemeChanged += (s, e) =>
ApplyTheme(e.RequestedTheme == AppTheme.Dark);
硬编码颜色会破坏主题效果
对于应该遵循主题的元素,避免使用内联颜色值:
<Label TextColor="#333333" />
<Label TextColor="{DynamicResource PrimaryTextColor}" />
CSS 主题无法在运行时切换
.NET MAUI 支持 CSS 样式,但基于 CSS 的主题无法动态切换。如需在运行时切换,请使用 ResourceDictionary 主题。
主题键必须在各字典间保持一致
某个主题字典中使用的每个 x:Key 都必须存在于所有其他主题字典中。缺少键会导致静默回退到默认值,从而造成外观不一致。
平台支持
| 平台 | 最低版本 |
|---|---|
| iOS | 13+ |
| Android | 10+ (API 29) |
| macOS Catalyst | 10.15+ |
| Windows | 10+ |
快速参考
- OS 浅色/深色模式 →
AppThemeBinding标记扩展 - C# 中的主题颜色 →
SetAppThemeColor()、SetAppTheme<T>() - 读取 OS 主题 →
Application.Current.RequestedTheme - 强制设置主题 →
Application.Current.UserAppTheme = AppTheme.Dark - 主题更改 →
RequestedThemeChanged事件 - 自定义切换 → 从
MergedDictionaries中Remove旧主题,再Add新主题——绝不要使用Clear() - 运行时绑定 →
DynamicResource(而非StaticResource) - 持久化选择 →
Preferences.Set/Preferences.Get