Files

609 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 主题系统
本文档详细说明如何在组件中实现主题切换,确保组件完美适配亮色和暗色主题。
## 🎨 主题系统概述
阑山桌面支持以下主题:
- **亮色主题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测试两种主题确保对比度适配图标图片。