mcpField
M
maui-theming

maui-theming

github.com/dotnet设计与 UI5.8k3.4k 次安装1 天前更新
代码仓库

关于 maui-theming

为 .NET MAUI 应用开发者提供主题设置指导,涵盖系统浅色/深色模式和自定义主题,并说明运行时切换、系统主题响应及偏好保存。

主要功能

  1. 01配置系统浅色和深色主题
  2. 02创建自定义 ResourceDictionary 主题
  3. 03运行时切换主题
  4. 04检测系统主题变化
  5. 05保存并恢复主题偏好

适用场景

  1. 01为 .NET MAUI 应用添加浅色和深色模式
  2. 02提供品牌主题选择
  3. 03响应操作系统主题变化
  4. 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 8 或更高版本的 .NET MAUI 项目
  • 需要支持主题样式的 XAML 页面或 C# UI 代码

工作流

  1. 检测项目当前采用的主题方案(AppThemeBinding、ResourceDictionary 或未使用主题方案)。
  2. 选择合适的策略:简单的浅色/深色主题使用 AppThemeBinding,自定义/多主题使用 ResourceDictionary 交换,也可结合使用两者。
  3. 定义主题资源——内联的 AppThemeBinding 值,或包含匹配键的独立 ResourceDictionary 文件。
  4. 在所有 XAML 页面中,将硬编码颜色替换为 DynamicResource 绑定(或 AppThemeBinding 标记扩展)。
  5. 使用 Application.Current.RequestedTheme 和 RequestedThemeChanged 事件检测系统主题。
  6. 使用 Preferences.Set / Preferences.Get 持久化用户偏好,并在启动时应用。
  7. 确认 Android 的 MainActivity 上设置了 ConfigChanges.UiMode,以避免主题变化时重启 Activity。
  8. 至少在一个目标平台上测试浅色和深色主题,确认所有 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 都必须存在于所有其他主题字典中。缺少键会导致静默回退到默认值,从而造成外观不一致。

平台支持

平台最低版本
iOS13+
Android10+ (API 29)
macOS Catalyst10.15+
Windows10+

快速参考

  • 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

评论

支持 Markdown