Files

18 KiB
Raw Permalink Blame History

主题系统

本文档详细说明如何在组件中实现主题切换,确保组件完美适配亮色和暗色主题。

🎨 主题系统概述

阑山桌面支持以下主题:

  • 亮色主题Light Theme - 默认主题,适合白天使用
  • 暗色主题Dark Theme - 保护眼睛,适合夜间使用
  • 跟随系统 - 自动跟随 Windows 系统主题

🏗️ 主题架构

主题资源结构

Themes/
├── LightTheme.axaml       # 亮色主题资源
├── DarkTheme.axaml        # 暗色主题资源
└── Common.axaml           # 通用资源(尺寸、字体等)

资源字典加载

<Application.Styles>
  <!-- 通用资源 -->
  <StyleInclude Source="avares://LanMountainDesktop/Themes/Common.axaml"/>
  
  <!-- 主题资源(动态加载) -->
  <StyleInclude Source="{DynamicResource CurrentTheme}"/>
</Application.Styles>

💡 亮色主题Light Theme

完整颜色定义

<ResourceDictionary xmlns="https://github.com/avaloniaui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  
  <!-- ========== 背景色 ========== -->
  <SolidColorBrush x:Key="DesktopBackgroundBrush" Color="#F3F3F3"/>
  <SolidColorBrush x:Key="CardBackgroundBrush" Color="#FFFFFF"/>
  <SolidColorBrush x:Key="CardBackgroundSecondaryBrush" Color="#F9F9F9"/>
  <SolidColorBrush x:Key="CardBackgroundHoverBrush" Color="#F3F3F3"/>
  <SolidColorBrush x:Key="CardBackgroundPressedBrush" Color="#E8E8E8"/>
  
  <!-- ========== 文本色 ========== -->
  <SolidColorBrush x:Key="TextFillColorPrimaryBrush" Color="#1C1C1C"/>
  <SolidColorBrush x:Key="TextFillColorSecondaryBrush" Color="#616161"/>
  <SolidColorBrush x:Key="TextFillColorTertiaryBrush" Color="#8E8E8E"/>
  <SolidColorBrush x:Key="TextFillColorDisabledBrush" Color="#C7C7C7"/>
  <SolidColorBrush x:Key="TextFillColorInverseBrush" Color="#FFFFFF"/>
  
  <!-- ========== 强调色 ========== -->
  <SolidColorBrush x:Key="AccentBrush" Color="#0078D4"/>
  <SolidColorBrush x:Key="AccentHoverBrush" Color="#106EBE"/>
  <SolidColorBrush x:Key="AccentPressedBrush" Color="#005A9E"/>
  <SolidColorBrush x:Key="AccentDisabledBrush" Color="#80BCEB"/>
  
  <!-- ========== 语义色 ========== -->
  <SolidColorBrush x:Key="SuccessBrush" Color="#107C10"/>
  <SolidColorBrush x:Key="WarningBrush" Color="#FF8C00"/>
  <SolidColorBrush x:Key="ErrorBrush" Color="#E81123"/>
  <SolidColorBrush x:Key="InfoBrush" Color="#0078D4"/>
  
  <!-- ========== 边框与分割线 ========== -->
  <SolidColorBrush x:Key="CardBorderBrush" Color="#E0E0E0"/>
  <SolidColorBrush x:Key="DividerBrush" Color="#EBEBEB"/>
  <SolidColorBrush x:Key="FocusBorderBrush" Color="#0078D4"/>
  
  <!-- ========== 输入框 ========== -->
  <SolidColorBrush x:Key="TextBoxBackgroundBrush" Color="#FFFFFF"/>
  <SolidColorBrush x:Key="TextBoxBorderBrush" Color="#E0E0E0"/>
  <SolidColorBrush x:Key="TextBoxBorderHoverBrush" Color="#C0C0C0"/>
  <SolidColorBrush x:Key="TextBoxBorderFocusBrush" Color="#0078D4"/>
  
  <!-- ========== 覆盖层 ========== -->
  <SolidColorBrush x:Key="OverlayBrush" Color="#80000000"/>
  <SolidColorBrush x:Key="TooltipBackgroundBrush" Color="#F9F9F9"/>
  
</ResourceDictionary>

亮色主题示例

┌──────────────────────────────────┐
│  ░░░░░░░░░ #F3F3F3 ░░░░░░░░░    │ 桌面背景
│  ┌────────────────────────────┐  │
│  │ 📍 北京        #1C1C1C     │  │ 主要文本
│  │                            │  │
│  │        ☀️                  │  │
│  │       25°C    #1C1C1C      │  │
│  │       晴天    #616161      │  │ 次要文本
│  │                            │  │
│  │ 今天天气不错  #8E8E8E      │  │ 辅助文本
│  │                            │  │
│  │            [🔄] [⚙️]      │  │
│  └────────────────────────────┘  │
│         #FFFFFF 卡片背景          │
└──────────────────────────────────┘

🌙 暗色主题Dark Theme

完整颜色定义

<ResourceDictionary xmlns="https://github.com/avaloniaui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  
  <!-- ========== 背景色 ========== -->
  <SolidColorBrush x:Key="DesktopBackgroundBrush" Color="#202020"/>
  <SolidColorBrush x:Key="CardBackgroundBrush" Color="#2C2C2C"/>
  <SolidColorBrush x:Key="CardBackgroundSecondaryBrush" Color="#343434"/>
  <SolidColorBrush x:Key="CardBackgroundHoverBrush" Color="#3A3A3A"/>
  <SolidColorBrush x:Key="CardBackgroundPressedBrush" Color="#404040"/>
  
  <!-- ========== 文本色 ========== -->
  <SolidColorBrush x:Key="TextFillColorPrimaryBrush" Color="#FFFFFF"/>
  <SolidColorBrush x:Key="TextFillColorSecondaryBrush" Color="#C8C8C8"/>
  <SolidColorBrush x:Key="TextFillColorTertiaryBrush" Color="#8E8E8E"/>
  <SolidColorBrush x:Key="TextFillColorDisabledBrush" Color="#5E5E5E"/>
  <SolidColorBrush x:Key="TextFillColorInverseBrush" Color="#1C1C1C"/>
  
  <!-- ========== 强调色 ========== -->
  <SolidColorBrush x:Key="AccentBrush" Color="#60CDFF"/>
  <SolidColorBrush x:Key="AccentHoverBrush" Color="#3DB8FF"/>
  <SolidColorBrush x:Key="AccentPressedBrush" Color="#1AA7FF"/>
  <SolidColorBrush x:Key="AccentDisabledBrush" Color="#306680"/>
  
  <!-- ========== 语义色 ========== -->
  <SolidColorBrush x:Key="SuccessBrush" Color="#6CCB5F"/>
  <SolidColorBrush x:Key="WarningBrush" Color="#FCE100"/>
  <SolidColorBrush x:Key="ErrorBrush" Color="#FF99A4"/>
  <SolidColorBrush x:Key="InfoBrush" Color="#60CDFF"/>
  
  <!-- ========== 边框与分割线 ========== -->
  <SolidColorBrush x:Key="CardBorderBrush" Color="#3F3F3F"/>
  <SolidColorBrush x:Key="DividerBrush" Color="#3A3A3A"/>
  <SolidColorBrush x:Key="FocusBorderBrush" Color="#60CDFF"/>
  
  <!-- ========== 输入框 ========== -->
  <SolidColorBrush x:Key="TextBoxBackgroundBrush" Color="#2C2C2C"/>
  <SolidColorBrush x:Key="TextBoxBorderBrush" Color="#3F3F3F"/>
  <SolidColorBrush x:Key="TextBoxBorderHoverBrush" Color="#505050"/>
  <SolidColorBrush x:Key="TextBoxBorderFocusBrush" Color="#60CDFF"/>
  
  <!-- ========== 覆盖层 ========== -->
  <SolidColorBrush x:Key="OverlayBrush" Color="#80000000"/>
  <SolidColorBrush x:Key="TooltipBackgroundBrush" Color="#343434"/>
  
</ResourceDictionary>

暗色主题示例

┌──────────────────────────────────┐
│  ▓▓▓▓▓▓▓▓▓ #202020 ▓▓▓▓▓▓▓▓▓    │ 桌面背景
│  ┌────────────────────────────┐  │
│  │ 📍 北京        #FFFFFF     │  │ 主要文本
│  │                            │  │
│  │        ☀️                  │  │
│  │       25°C    #FFFFFF      │  │
│  │       晴天    #C8C8C8      │  │ 次要文本
│  │                            │  │
│  │ 今天天气不错  #8E8E8E      │  │ 辅助文本
│  │                            │  │
│  │            [🔄] [⚙️]      │  │
│  └────────────────────────────┘  │
│         #2C2C2C 卡片背景          │
└──────────────────────────────────┘

🔄 主题切换实现

在组件中使用主题资源

<Border Background="{DynamicResource CardBackgroundBrush}"
        BorderBrush="{DynamicResource CardBorderBrush}"
        BorderThickness="1"
        CornerRadius="8"
        Padding="16">
  
  <StackPanel Spacing="8">
    <!-- 标题 -->
    <TextBlock Text="天气预报"
               FontSize="16"
               FontWeight="SemiBold"
               Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
    
    <!-- 内容 -->
    <TextBlock Text="今天天气晴朗"
               FontSize="14"
               Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
    
    <!-- 按钮 -->
    <Button Content="刷新"
            Background="{DynamicResource AccentBrush}"
            Foreground="White"/>
  </StackPanel>
  
</Border>

关键点:使用 DynamicResource

<!-- ✅ 正确:使用 DynamicResource -->
<Border Background="{DynamicResource CardBackgroundBrush}">
  <!-- 会响应主题切换 -->
</Border>

<!-- ❌ 错误:使用 StaticResource -->
<Border Background="{StaticResource CardBackgroundBrush}">
  <!-- 不会响应主题切换 -->
</Border>

<!-- ❌ 错误:硬编码颜色 -->
<Border Background="#FFFFFF">
  <!-- 完全不支持主题 -->
</Border>

监听主题变更

public class WeatherComponent : ComponentBase
{
    public override async Task InitializeAsync()
    {
        // 订阅主题变更事件
        var themeService = Services.GetService<IThemeService>();
        if (themeService != null)
        {
            themeService.ThemeChanged += OnThemeChanged;
        }
    }
    
    private void OnThemeChanged(object? sender, ThemeChangedEventArgs e)
    {
        Logger.LogInformation($"Theme changed to: {e.NewTheme}");
        
        // 执行主题切换后的逻辑
        // 例如:重新加载图片、调整布局等
        UpdateForTheme(e.NewTheme);
    }
    
    private void UpdateForTheme(Theme theme)
    {
        if (theme == Theme.Dark)
        {
            // 暗色主题特殊处理
            LoadDarkThemeIcon();
        }
        else
        {
            // 亮色主题特殊处理
            LoadLightThemeIcon();
        }
    }
    
    public override void Dispose()
    {
        // 取消订阅
        var themeService = Services.GetService<IThemeService>();
        if (themeService != null)
        {
            themeService.ThemeChanged -= OnThemeChanged;
        }
        
        base.Dispose();
    }
}

🖼️ 图片与图标适配

图标适配方案

方案 1: 使用 Emoji推荐

<!-- Emoji 自动适配主题 -->
<TextBlock Text="☀️" FontSize="48"/>
<TextBlock Text="🌙" FontSize="48"/>

优点:

  • 无需额外资源
  • 自动适配主题
  • 跨平台显示一致

方案 2: 使用颜色可变的图标

<!-- Path 图标,颜色跟随主题 -->
<Path Data="M12 2L2 7l10 5 10-5-10-5z..."
      Fill="{DynamicResource TextFillColorPrimaryBrush}"
      Width="24"
      Height="24"/>

优点:

  • 完美适配主题
  • 矢量图形,清晰度高
  • 可自定义样式

方案 3: 提供两套图片

public class IconHelper
{
    public static string GetThemedIcon(string iconName, Theme theme)
    {
        if (theme == Theme.Dark)
        {
            return $"avares://MyPlugin/Assets/Icons/Dark/{iconName}.png";
        }
        else
        {
            return $"avares://MyPlugin/Assets/Icons/Light/{iconName}.png";
        }
    }
}
<Image Source="{Binding ThemedIconPath}"
       Width="24"
       Height="24"/>

目录结构:

Assets/
├── Icons/
│   ├── Light/
│   │   ├── weather.png
│   │   └── settings.png
│   └── Dark/
│       ├── weather.png
│       └── settings.png

图片适配示例

public class WeatherComponent : ComponentBase
{
    private string _weatherIconPath = "";
    
    public string WeatherIconPath
    {
        get => _weatherIconPath;
        set => SetProperty(ref _weatherIconPath, value);
    }
    
    public override async Task InitializeAsync()
    {
        // 初始化图标
        UpdateWeatherIcon();
        
        // 订阅主题变更
        var themeService = Services.GetService<IThemeService>();
        if (themeService != null)
        {
            themeService.ThemeChanged += (s, e) => UpdateWeatherIcon();
        }
    }
    
    private void UpdateWeatherIcon()
    {
        var themeService = Services.GetService<IThemeService>();
        var currentTheme = themeService?.CurrentTheme ?? Theme.Light;
        
        var themePath = currentTheme == Theme.Dark ? "Dark" : "Light";
        WeatherIconPath = $"avares://MyPlugin/Assets/Icons/{themePath}/sunny.png";
    }
}

🎨 自定义主题

扩展主题系统

public class CustomTheme
{
    public string Name { get; set; } = "";
    public Dictionary<string, Color> Colors { get; set; } = new();
    
    public void Apply()
    {
        var resources = Application.Current!.Resources;
        
        foreach (var (key, color) in Colors)
        {
            resources[key] = new SolidColorBrush(color);
        }
    }
}

// 使用自定义主题
var customTheme = new CustomTheme
{
    Name = "Ocean Blue",
    Colors = new Dictionary<string, Color>
    {
        ["CardBackgroundBrush"] = Color.FromRgb(230, 240, 255),
        ["AccentBrush"] = Color.FromRgb(0, 120, 215),
        ["TextFillColorPrimaryBrush"] = Color.FromRgb(28, 28, 28)
    }
};

customTheme.Apply();

用户自定义颜色

public class ThemeCustomizationService
{
    public void SetCustomAccentColor(Color color)
    {
        var resources = Application.Current!.Resources;
        
        // 更新强调色
        resources["AccentBrush"] = new SolidColorBrush(color);
        
        // 自动生成悬停和按下颜色
        var hoverColor = DarkenColor(color, 0.1);
        var pressedColor = DarkenColor(color, 0.2);
        
        resources["AccentHoverBrush"] = new SolidColorBrush(hoverColor);
        resources["AccentPressedBrush"] = new SolidColorBrush(pressedColor);
    }
    
    private Color DarkenColor(Color color, double factor)
    {
        return Color.FromRgb(
            (byte)(color.R * (1 - factor)),
            (byte)(color.G * (1 - factor)),
            (byte)(color.B * (1 - factor))
        );
    }
}

🔍 主题测试

测试清单

public class ThemeTestHelper
{
    public static async Task<List<string>> ValidateThemeSupport(Control component)
    {
        var issues = new List<string>();
        
        // 测试亮色主题
        SwitchTheme(Theme.Light);
        await Task.Delay(100);
        issues.AddRange(CheckContrast(component, Theme.Light));
        
        // 测试暗色主题
        SwitchTheme(Theme.Dark);
        await Task.Delay(100);
        issues.AddRange(CheckContrast(component, Theme.Dark));
        
        return issues;
    }
    
    private static List<string> CheckContrast(Control component, Theme theme)
    {
        var issues = new List<string>();
        
        // 检查文本对比度
        var textBlocks = component.GetVisualDescendants()
            .OfType<TextBlock>();
        
        foreach (var textBlock in textBlocks)
        {
            var foreground = GetColor(textBlock.Foreground);
            var background = GetBackgroundColor(textBlock);
            
            var contrast = CalculateContrast(foreground, background);
            if (contrast < 4.5)
            {
                issues.Add($"Low contrast in {theme} theme: {contrast:F2}:1");
            }
        }
        
        return issues;
    }
    
    private static double CalculateContrast(Color fg, Color bg)
    {
        var l1 = GetRelativeLuminance(fg);
        var l2 = GetRelativeLuminance(bg);
        
        var lighter = Math.Max(l1, l2);
        var darker = Math.Min(l1, l2);
        
        return (lighter + 0.05) / (darker + 0.05);
    }
    
    private static double GetRelativeLuminance(Color color)
    {
        var r = GetLuminanceComponent(color.R / 255.0);
        var g = GetLuminanceComponent(color.G / 255.0);
        var b = GetLuminanceComponent(color.B / 255.0);
        
        return 0.2126 * r + 0.7152 * g + 0.0722 * b;
    }
    
    private static double GetLuminanceComponent(double c)
    {
        return c <= 0.03928 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4);
    }
}

主题适配检查清单

发布前请确保:

颜色资源

  • 所有颜色使用 DynamicResource
  • 没有硬编码的颜色值
  • 使用系统提供的颜色资源
  • 自定义颜色定义了亮色和暗色两个版本

文本对比度

  • 亮色主题下文本对比度 ≥ 4.5:1
  • 暗色主题下文本对比度 ≥ 4.5:1
  • 大号文本对比度 ≥ 3:1
  • UI 元素对比度 ≥ 3:1

图标与图片

  • 图标适配亮色主题
  • 图标适配暗色主题
  • 图片在两种主题下都清晰可见
  • 没有使用会"消失"的白色/黑色图标

交互状态

  • 悬停状态在两种主题下都清晰
  • 按下状态在两种主题下都清晰
  • 聚焦状态在两种主题下都清晰
  • 禁用状态在两种主题下都清晰

实际测试

  • 在亮色主题下运行并检查
  • 在暗色主题下运行并检查
  • 切换主题时无闪烁或错误
  • 长时间使用眼睛舒适

🎓 最佳实践

DO - 应该这样做

<!-- ✅ 使用 DynamicResource -->
<Border Background="{DynamicResource CardBackgroundBrush}">
  <TextBlock Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
</Border>

<!-- ✅ 使用语义化的资源名称 -->
<Button Background="{DynamicResource AccentBrush}"/>
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>

<!-- ✅ 订阅主题变更事件 -->
themeService.ThemeChanged += OnThemeChanged;

DON'T - 不应该这样做

<!-- ❌ 硬编码颜色 -->
<Border Background="#FFFFFF">
  <TextBlock Foreground="#000000"/>
</Border>

<!-- ❌ 使用 StaticResource -->
<Border Background="{StaticResource CardBackgroundBrush}">
  <!-- 不会响应主题切换 -->
</Border>

<!-- ❌ 假设总是亮色主题 -->
<Image Source="avares://MyPlugin/Assets/white-icon.png"/>
<!-- 在暗色主题下看不见 -->

📖 相关文档


记住: 使用 DynamicResource测试两种主题确保对比度适配图标图片。