Files
LanMountainDesktop/docs/03-组件设计规范/05-主题系统.md

609 lines
18 KiB
Markdown
Raw Normal View History

# 主题系统
本文档详细说明如何在组件中实现主题切换,确保组件完美适配亮色和暗色主题。
## 🎨 主题系统概述
阑山桌面支持以下主题:
- **亮色主题Light Theme** - 默认主题,适合白天使用
- **暗色主题Dark Theme** - 保护眼睛,适合夜间使用
- **跟随系统** - 自动跟随 Windows 系统主题
## 🏗️ 主题架构
### 主题资源结构
```
Themes/
├── LightTheme.axaml # 亮色主题资源
├── DarkTheme.axaml # 暗色主题资源
└── Common.axaml # 通用资源(尺寸、字体等)
```
### 资源字典加载
```xml
<Application.Styles>
<!-- 通用资源 -->
<StyleInclude Source="avares://LanMountainDesktop/Themes/Common.axaml"/>
<!-- 主题资源(动态加载) -->
<StyleInclude Source="{DynamicResource CurrentTheme}"/>
</Application.Styles>
```
## 💡 亮色主题Light Theme
### 完整颜色定义
```xml
<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
### 完整颜色定义
```xml
<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 卡片背景 │
└──────────────────────────────────┘
```
## 🔄 主题切换实现
### 在组件中使用主题资源
```xml
<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
```xml
<!-- ✅ 正确:使用 DynamicResource -->
<Border Background="{DynamicResource CardBackgroundBrush}">
<!-- 会响应主题切换 -->
</Border>
<!-- ❌ 错误:使用 StaticResource -->
<Border Background="{StaticResource CardBackgroundBrush}">
<!-- 不会响应主题切换 -->
</Border>
<!-- ❌ 错误:硬编码颜色 -->
<Border Background="#FFFFFF">
<!-- 完全不支持主题 -->
</Border>
```
### 监听主题变更
```csharp
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推荐
```xml
<!-- Emoji 自动适配主题 -->
<TextBlock Text="☀️" FontSize="48"/>
<TextBlock Text="🌙" FontSize="48"/>
```
**优点**:
- ✅ 无需额外资源
- ✅ 自动适配主题
- ✅ 跨平台显示一致
#### 方案 2: 使用颜色可变的图标
```xml
<!-- Path 图标,颜色跟随主题 -->
<Path Data="M12 2L2 7l10 5 10-5-10-5z..."
Fill="{DynamicResource TextFillColorPrimaryBrush}"
Width="24"
Height="24"/>
```
**优点**:
- ✅ 完美适配主题
- ✅ 矢量图形,清晰度高
- ✅ 可自定义样式
#### 方案 3: 提供两套图片
```csharp
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";
}
}
}
```
```xml
<Image Source="{Binding ThemedIconPath}"
Width="24"
Height="24"/>
```
**目录结构**:
```
Assets/
├── Icons/
│ ├── Light/
│ │ ├── weather.png
│ │ └── settings.png
│ └── Dark/
│ ├── weather.png
│ └── settings.png
```
### 图片适配示例
```csharp
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";
}
}
```
## 🎨 自定义主题
### 扩展主题系统
```csharp
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();
```
### 用户自定义颜色
```csharp
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))
);
}
}
```
## 🔍 主题测试
### 测试清单
```csharp
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 - 应该这样做
```xml
<!-- ✅ 使用 DynamicResource -->
<Border Background="{DynamicResource CardBackgroundBrush}">
<TextBlock Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
</Border>
<!-- ✅ 使用语义化的资源名称 -->
<Button Background="{DynamicResource AccentBrush}"/>
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
<!-- ✅ 订阅主题变更事件 -->
themeService.ThemeChanged += OnThemeChanged;
```
### DON'T - 不应该这样做
```xml
<!-- ❌ 硬编码颜色 -->
<Border Background="#FFFFFF">
<TextBlock Foreground="#000000"/>
</Border>
<!-- ❌ 使用 StaticResource -->
<Border Background="{StaticResource CardBackgroundBrush}">
<!-- 不会响应主题切换 -->
</Border>
<!-- ❌ 假设总是亮色主题 -->
<Image Source="avares://MyPlugin/Assets/white-icon.png"/>
<!-- 在暗色主题下看不见 -->
```
## 📖 相关文档
- [视觉规范](02-视觉规范.md) - 完整的颜色系统
- [布局规范](03-布局规范.md) - 安全区域和间距
- [交互规范](04-交互规范.md) - 交互状态和动画
- [组件系统](../01-插件开发/02-核心概念/02-组件系统.md) - 组件开发
---
**记住**: 使用 DynamicResource测试两种主题确保对比度适配图标图片。