# 主题系统
本文档详细说明如何在组件中实现主题切换,确保组件完美适配亮色和暗色主题。
## 🎨 主题系统概述
阑山桌面支持以下主题:
- **亮色主题(Light Theme)** - 默认主题,适合白天使用
- **暗色主题(Dark Theme)** - 保护眼睛,适合夜间使用
- **跟随系统** - 自动跟随 Windows 系统主题
## 🏗️ 主题架构
### 主题资源结构
```
Themes/
├── LightTheme.axaml # 亮色主题资源
├── DarkTheme.axaml # 暗色主题资源
└── Common.axaml # 通用资源(尺寸、字体等)
```
### 资源字典加载
```xml
```
## 💡 亮色主题(Light Theme)
### 完整颜色定义
```xml
```
### 亮色主题示例
```
┌──────────────────────────────────┐
│ ░░░░░░░░░ #F3F3F3 ░░░░░░░░░ │ 桌面背景
│ ┌────────────────────────────┐ │
│ │ 📍 北京 #1C1C1C │ │ 主要文本
│ │ │ │
│ │ ☀️ │ │
│ │ 25°C #1C1C1C │ │
│ │ 晴天 #616161 │ │ 次要文本
│ │ │ │
│ │ 今天天气不错 #8E8E8E │ │ 辅助文本
│ │ │ │
│ │ [🔄] [⚙️] │ │
│ └────────────────────────────┘ │
│ #FFFFFF 卡片背景 │
└──────────────────────────────────┘
```
## 🌙 暗色主题(Dark Theme)
### 完整颜色定义
```xml
```
### 暗色主题示例
```
┌──────────────────────────────────┐
│ ▓▓▓▓▓▓▓▓▓ #202020 ▓▓▓▓▓▓▓▓▓ │ 桌面背景
│ ┌────────────────────────────┐ │
│ │ 📍 北京 #FFFFFF │ │ 主要文本
│ │ │ │
│ │ ☀️ │ │
│ │ 25°C #FFFFFF │ │
│ │ 晴天 #C8C8C8 │ │ 次要文本
│ │ │ │
│ │ 今天天气不错 #8E8E8E │ │ 辅助文本
│ │ │ │
│ │ [🔄] [⚙️] │ │
│ └────────────────────────────┘ │
│ #2C2C2C 卡片背景 │
└──────────────────────────────────┘
```
## 🔄 主题切换实现
### 在组件中使用主题资源
```xml
```
### 关键点:使用 DynamicResource
```xml
```
### 监听主题变更
```csharp
public class WeatherComponent : ComponentBase
{
public override async Task InitializeAsync()
{
// 订阅主题变更事件
var themeService = Services.GetService();
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();
if (themeService != null)
{
themeService.ThemeChanged -= OnThemeChanged;
}
base.Dispose();
}
}
```
## 🖼️ 图片与图标适配
### 图标适配方案
#### 方案 1: 使用 Emoji(推荐)
```xml
```
**优点**:
- ✅ 无需额外资源
- ✅ 自动适配主题
- ✅ 跨平台显示一致
#### 方案 2: 使用颜色可变的图标
```xml
```
**优点**:
- ✅ 完美适配主题
- ✅ 矢量图形,清晰度高
- ✅ 可自定义样式
#### 方案 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
```
**目录结构**:
```
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();
if (themeService != null)
{
themeService.ThemeChanged += (s, e) => UpdateWeatherIcon();
}
}
private void UpdateWeatherIcon()
{
var themeService = Services.GetService();
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 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
{
["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> ValidateThemeSupport(Control component)
{
var issues = new List();
// 测试亮色主题
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 CheckContrast(Control component, Theme theme)
{
var issues = new List();
// 检查文本对比度
var textBlocks = component.GetVisualDescendants()
.OfType();
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
themeService.ThemeChanged += OnThemeChanged;
```
### DON'T - 不应该这样做
```xml
```
## 📖 相关文档
- [视觉规范](02-视觉规范.md) - 完整的颜色系统
- [布局规范](03-布局规范.md) - 安全区域和间距
- [交互规范](04-交互规范.md) - 交互状态和动画
- [组件系统](../01-插件开发/02-核心概念/02-组件系统.md) - 组件开发
---
**记住**: 使用 DynamicResource,测试两种主题,确保对比度,适配图标图片。