Files
LanMountainDesktop/docs/01-插件开发/02-核心概念/03-设置系统.md
2026-06-08 12:18:58 +08:00

859 lines
23 KiB
Markdown
Raw 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.
# 设置系统
本文档介绍阑山桌面的设置系统,包括配置管理、持久化、设置页面和最佳实践。
## 设置系统概览
阑山桌面提供了统一的设置系统,用于管理应用、插件和组件的配置数据。
### 核心特性
- 💾 **自动持久化** - 设置自动保存到本地
- 🔔 **变更通知** - 监听设置变更事件
- 📁 **分域管理** - 按命名空间组织设置
- 🔒 **类型安全** - 泛型 API 保证类型安全
- 🎨 **UI 集成** - 轻松创建设置页面
### 设置存储位置
```
%LOCALAPPDATA%\LanMountainDesktop\
└── settings\
├── app.json # 应用设置
├── appearance.json # 外观设置
├── plugins\
│ ├── com.example.plugin1.json
│ └── com.example.plugin2.json
└── components\
└── com.example.plugin1.component1.json
```
## 使用设置服务
### 在插件中使用
```csharp
public class MyPlugin : IPlugin
{
private IPluginContext? _context;
public async Task InitializeAsync(IPluginContext context)
{
_context = context;
// 通过 context 访问设置
var settings = context.Settings;
// 读取设置
var apiKey = settings.GetValue("ApiKey", "");
var refreshRate = settings.GetValue("RefreshRate", 60);
var enableNotifications = settings.GetValue("EnableNotifications", true);
// 保存设置
settings.SetValue("LastStartTime", DateTime.Now);
}
}
```
### 在组件中使用
```csharp
public class MyComponent : ComponentBase
{
public override Task InitializeAsync()
{
// 组件有自己的设置域
// 自动命名空间:{PluginId}.{ComponentId}
// 读取设置
var location = Settings.GetValue("Location", "北京");
var useFahrenheit = Settings.GetValue("UseFahrenheit", false);
// 读取复杂对象
var config = Settings.GetValue<ComponentConfig>("Config", new ComponentConfig());
return Task.CompletedTask;
}
public void UpdateLocation(string location)
{
Location = location;
// 保存设置
Settings.SetValue("Location", location);
}
}
```
## 设置 API
### ISettingsService 接口
```csharp
public interface ISettingsService
{
/// <summary>
/// 获取设置值
/// </summary>
T GetValue<T>(string key, T defaultValue);
/// <summary>
/// 设置值
/// </summary>
void SetValue<T>(string key, T value);
/// <summary>
/// 删除设置
/// </summary>
void Remove(string key);
/// <summary>
/// 检查设置是否存在
/// </summary>
bool Contains(string key);
/// <summary>
/// 获取所有键
/// </summary>
IEnumerable<string> GetAllKeys();
/// <summary>
/// 清空所有设置
/// </summary>
void Clear();
/// <summary>
/// 设置变更事件
/// </summary>
event EventHandler<SettingChangedEventArgs>? SettingChanged;
}
```
### 基本用法
```csharp
// 读取设置
var value = settings.GetValue<string>("Key", "DefaultValue");
// 保存设置
settings.SetValue("Key", "NewValue");
// 删除设置
settings.Remove("Key");
// 检查是否存在
if (settings.Contains("Key"))
{
// ...
}
// 获取所有键
var keys = settings.GetAllKeys();
// 清空所有设置
settings.Clear();
```
## 支持的数据类型
### 基本类型
```csharp
// 字符串
settings.SetValue("Name", "张三");
var name = settings.GetValue("Name", "");
// 数字
settings.SetValue("Age", 25);
var age = settings.GetValue("Age", 0);
settings.SetValue("Price", 99.99);
var price = settings.GetValue("Price", 0.0);
// 布尔值
settings.SetValue("Enabled", true);
var enabled = settings.GetValue("Enabled", false);
// 日期时间
settings.SetValue("LastUpdate", DateTime.Now);
var lastUpdate = settings.GetValue("LastUpdate", DateTime.MinValue);
// 枚举
settings.SetValue("Theme", AppTheme.Dark);
var theme = settings.GetValue("Theme", AppTheme.Light);
```
### 复杂对象
```csharp
// 定义配置类
public class WeatherConfig
{
public string City { get; set; } = "北京";
public string Unit { get; set; } = "Celsius";
public int RefreshInterval { get; set; } = 10;
public List<string> FavoriteCities { get; set; } = new();
}
// 保存对象
var config = new WeatherConfig
{
City = "上海",
Unit = "Celsius",
RefreshInterval = 15,
FavoriteCities = new List<string> { "北京", "上海", "广州" }
};
settings.SetValue("WeatherConfig", config);
// 读取对象
var savedConfig = settings.GetValue<WeatherConfig>(
"WeatherConfig",
new WeatherConfig()
);
```
### 集合类型
```csharp
// 列表
var favoriteColors = new List<string> { "红色", "蓝色", "绿色" };
settings.SetValue("FavoriteColors", favoriteColors);
var colors = settings.GetValue<List<string>>("FavoriteColors", new List<string>());
// 字典
var preferences = new Dictionary<string, string>
{
["Language"] = "zh-CN",
["Timezone"] = "Asia/Shanghai"
};
settings.SetValue("Preferences", preferences);
var prefs = settings.GetValue<Dictionary<string, string>>(
"Preferences",
new Dictionary<string, string>()
);
```
## 监听设置变更
### 订阅变更事件
```csharp
public class MyPlugin : IPlugin
{
private ISettingsService? _settings;
public async Task InitializeAsync(IPluginContext context)
{
_settings = context.Settings;
// 订阅设置变更事件
_settings.SettingChanged += OnSettingChanged;
}
private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
{
// e.Key - 变更的设置键
// e.OldValue - 旧值
// e.NewValue - 新值
if (e.Key == "ApiKey")
{
var newApiKey = e.NewValue as string;
_logger.LogInformation($"API Key changed to: {newApiKey}");
// 重新初始化服务
ReinitializeService(newApiKey);
}
}
public async Task ShutdownAsync()
{
// 取消订阅(防止内存泄漏)
if (_settings != null)
{
_settings.SettingChanged -= OnSettingChanged;
}
}
}
```
### 在组件中监听
```csharp
public class MyComponent : ComponentBase
{
public override Task InitializeAsync()
{
// 监听设置变更
Settings.SettingChanged += OnSettingChanged;
return Task.CompletedTask;
}
private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
{
switch (e.Key)
{
case "Location":
Location = e.NewValue as string ?? "北京";
_ = RefreshWeatherAsync();
break;
case "UseFahrenheit":
UseFahrenheit = (bool)(e.NewValue ?? false);
OnPropertyChanged(nameof(DisplayTemperature));
break;
}
}
public override void Dispose()
{
// 取消订阅
Settings.SettingChanged -= OnSettingChanged;
base.Dispose();
}
}
```
## 创建设置页面
### 步骤 1: 创建设置页视图
创建 `Settings/MyPluginSettingsPage.axaml`
```xml
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyPlugin.ViewModels"
x:Class="MyPlugin.Settings.MyPluginSettingsPage"
x:DataType="vm:MyPluginSettingsViewModel">
<ScrollViewer>
<StackPanel Spacing="16" Margin="24">
<!-- 页面标题 -->
<TextBlock Text="天气插件设置"
FontSize="24"
FontWeight="Bold"
Margin="0,0,0,8" />
<!-- 基本设置 -->
<Border Background="{DynamicResource CardBackgroundBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="16"
BorderBrush="{DynamicResource CardBorderBrush}"
BorderThickness="1">
<StackPanel Spacing="12">
<!-- 分组标题 -->
<TextBlock Text="基本设置"
FontSize="16"
FontWeight="SemiBold" />
<!-- 城市设置 -->
<StackPanel Spacing="8">
<TextBlock Text="城市:" />
<TextBox Text="{Binding Location, Mode=TwoWay}"
Watermark="输入城市名称"
Width="300"
HorizontalAlignment="Left" />
</StackPanel>
<!-- API Key -->
<StackPanel Spacing="8">
<TextBlock Text="API Key:" />
<TextBox Text="{Binding ApiKey, Mode=TwoWay}"
Watermark="输入 API Key"
PasswordChar="●"
Width="300"
HorizontalAlignment="Left" />
<TextBlock Text="从 https://api.weather.com 获取"
FontSize="12"
Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</StackPanel>
</Border>
<!-- 显示设置 -->
<Border Background="{DynamicResource CardBackgroundBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="16"
BorderBrush="{DynamicResource CardBorderBrush}"
BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="显示设置"
FontSize="16"
FontWeight="SemiBold" />
<!-- 温度单位 -->
<StackPanel Spacing="8">
<TextBlock Text="温度单位:" />
<ComboBox SelectedIndex="{Binding TemperatureUnitIndex, Mode=TwoWay}"
Width="200"
HorizontalAlignment="Left">
<ComboBoxItem Content="摄氏度 (°C)" />
<ComboBoxItem Content="华氏度 (°F)" />
</ComboBox>
</StackPanel>
<!-- 刷新间隔 -->
<StackPanel Spacing="8">
<TextBlock Text="刷新间隔 (分钟):" />
<NumericUpDown Value="{Binding RefreshInterval, Mode=TwoWay}"
Minimum="5"
Maximum="60"
Increment="5"
Width="200"
HorizontalAlignment="Left" />
</StackPanel>
<!-- 开关选项 -->
<CheckBox IsChecked="{Binding ShowIcon, Mode=TwoWay}"
Content="显示天气图标" />
<CheckBox IsChecked="{Binding EnableNotifications, Mode=TwoWay}"
Content="启用天气预警通知" />
</StackPanel>
</Border>
<!-- 高级设置 -->
<Border Background="{DynamicResource CardBackgroundBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="16"
BorderBrush="{DynamicResource CardBorderBrush}"
BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="高级设置"
FontSize="16"
FontWeight="SemiBold" />
<!-- 收藏城市 -->
<StackPanel Spacing="8">
<TextBlock Text="收藏城市:" />
<ListBox ItemsSource="{Binding FavoriteCities}"
Height="150"
Width="300"
HorizontalAlignment="Left" />
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBox x:Name="NewCityTextBox"
Watermark="添加城市"
Width="200" />
<Button Content="添加"
Command="{Binding AddCityCommand}"
CommandParameter="{Binding #NewCityTextBox.Text}" />
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
<!-- 操作按钮 -->
<StackPanel Orientation="Horizontal" Spacing="12">
<Button Content="保存"
Command="{Binding SaveCommand}"
IsDefault="True" />
<Button Content="重置"
Command="{Binding ResetCommand}" />
<Button Content="测试连接"
Command="{Binding TestConnectionCommand}" />
</StackPanel>
<!-- 状态提示 -->
<TextBlock Text="{Binding StatusMessage}"
Foreground="{Binding StatusColor}"
IsVisible="{Binding !!StatusMessage}" />
</StackPanel>
</ScrollViewer>
</UserControl>
```
### 步骤 2: 创建设置页视图模型
创建 `ViewModels/MyPluginSettingsViewModel.cs`
```csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
namespace MyPlugin.ViewModels;
public partial class MyPluginSettingsViewModel : ObservableObject
{
private readonly ISettingsService _settings;
private readonly ILogger _logger;
public MyPluginSettingsViewModel(
ISettingsService settings,
ILogger logger)
{
_settings = settings;
_logger = logger;
// 加载设置
LoadSettings();
}
// === 属性 ===
[ObservableProperty]
private string _location = "北京";
[ObservableProperty]
private string _apiKey = "";
[ObservableProperty]
private int _temperatureUnitIndex = 0;
[ObservableProperty]
private int _refreshInterval = 10;
[ObservableProperty]
private bool _showIcon = true;
[ObservableProperty]
private bool _enableNotifications = true;
[ObservableProperty]
private ObservableCollection<string> _favoriteCities = new();
[ObservableProperty]
private string? _statusMessage;
[ObservableProperty]
private string _statusColor = "Green";
// === 命令 ===
/// <summary>
/// 保存命令
/// </summary>
[RelayCommand]
private void Save()
{
try
{
// 保存所有设置
_settings.SetValue("Location", Location);
_settings.SetValue("ApiKey", ApiKey);
_settings.SetValue("UseFahrenheit", TemperatureUnitIndex == 1);
_settings.SetValue("RefreshInterval", RefreshInterval);
_settings.SetValue("ShowIcon", ShowIcon);
_settings.SetValue("EnableNotifications", EnableNotifications);
_settings.SetValue("FavoriteCities", FavoriteCities.ToList());
ShowStatus("设置已保存", "Green");
_logger.LogInformation("Settings saved successfully");
}
catch (Exception ex)
{
ShowStatus($"保存失败: {ex.Message}", "Red");
_logger.LogError(ex, "Failed to save settings");
}
}
/// <summary>
/// 重置命令
/// </summary>
[RelayCommand]
private void Reset()
{
// 重新加载设置
LoadSettings();
ShowStatus("已重置到上次保存的值", "Orange");
}
/// <summary>
/// 添加城市命令
/// </summary>
[RelayCommand]
private void AddCity(string? city)
{
if (string.IsNullOrWhiteSpace(city))
return;
if (!FavoriteCities.Contains(city))
{
FavoriteCities.Add(city);
ShowStatus($"已添加城市: {city}", "Green");
}
else
{
ShowStatus("城市已存在", "Orange");
}
}
/// <summary>
/// 测试连接命令
/// </summary>
[RelayCommand]
private async Task TestConnectionAsync()
{
ShowStatus("正在测试连接...", "Blue");
try
{
// 测试 API 连接
var result = await TestWeatherApiAsync(ApiKey, Location);
if (result)
{
ShowStatus("连接成功!", "Green");
}
else
{
ShowStatus("连接失败,请检查 API Key 和城市名称", "Red");
}
}
catch (Exception ex)
{
ShowStatus($"测试失败: {ex.Message}", "Red");
_logger.LogError(ex, "Connection test failed");
}
}
// === 辅助方法 ===
private void LoadSettings()
{
Location = _settings.GetValue("Location", "北京");
ApiKey = _settings.GetValue("ApiKey", "");
var useFahrenheit = _settings.GetValue("UseFahrenheit", false);
TemperatureUnitIndex = useFahrenheit ? 1 : 0;
RefreshInterval = _settings.GetValue("RefreshInterval", 10);
ShowIcon = _settings.GetValue("ShowIcon", true);
EnableNotifications = _settings.GetValue("EnableNotifications", true);
var cities = _settings.GetValue<List<string>>("FavoriteCities", new List<string>());
FavoriteCities = new ObservableCollection<string>(cities);
}
private void ShowStatus(string message, string color)
{
StatusMessage = message;
StatusColor = color;
// 3 秒后清除状态
Task.Delay(3000).ContinueWith(_ =>
{
StatusMessage = null;
});
}
private async Task<bool> TestWeatherApiAsync(string apiKey, string location)
{
// 实际实现中测试 API 连接
await Task.Delay(1000);
return !string.IsNullOrEmpty(apiKey);
}
}
```
### 步骤 3: 注册设置页
在插件入口注册:
```csharp
public class MyPlugin : IPlugin
{
public async Task InitializeAsync(IPluginContext context)
{
var settingsRegistry = context.Services
.GetService<ISettingsPageRegistry>();
if (settingsRegistry != null)
{
// 注册设置页
settingsRegistry.RegisterPage(
title: "天气插件",
category: "插件",
icon: "avares://MyPlugin/Assets/settings-icon.png",
pageFactory: () =>
{
var viewModel = new MyPluginSettingsViewModel(
context.Settings,
context.Logger
);
return new MyPluginSettingsPage
{
DataContext = viewModel
};
}
);
context.Logger.LogInformation("Settings page registered");
}
}
}
```
## 设置最佳实践
### ✅ 提供默认值
```csharp
// ✅ 好:提供合理的默认值
var timeout = settings.GetValue("Timeout", 30);
var apiUrl = settings.GetValue("ApiUrl", "https://api.example.com");
// ❌ 差:不提供默认值
var timeout = settings.GetValue<int>("Timeout", 0); // 0 可能不合理
```
### ✅ 验证设置值
```csharp
// ✅ 好:验证设置值
public void SetRefreshInterval(int minutes)
{
if (minutes < 1 || minutes > 60)
{
throw new ArgumentOutOfRangeException(
nameof(minutes),
"刷新间隔必须在 1-60 分钟之间"
);
}
RefreshInterval = minutes;
Settings.SetValue("RefreshInterval", minutes);
}
// ❌ 差:不验证
public void SetRefreshInterval(int minutes)
{
Settings.SetValue("RefreshInterval", minutes); // 可能是非法值
}
```
### ✅ 使用类型化配置
```csharp
// ✅ 好:使用强类型配置类
public class PluginConfig
{
public string ApiKey { get; set; } = "";
public string Location { get; set; } = "北京";
public int RefreshInterval { get; set; } = 10;
public bool EnableNotifications { get; set; } = true;
public void Validate()
{
if (string.IsNullOrEmpty(ApiKey))
throw new InvalidOperationException("API Key is required");
if (RefreshInterval < 1 || RefreshInterval > 60)
throw new ArgumentOutOfRangeException(nameof(RefreshInterval));
}
}
// 使用
var config = settings.GetValue<PluginConfig>("Config", new PluginConfig());
config.Validate();
// ❌ 差:分散的设置键
var apiKey = settings.GetValue<string>("ApiKey", "");
var location = settings.GetValue<string>("Location", "");
var interval = settings.GetValue<int>("RefreshInterval", 10);
```
### ✅ 取消事件订阅
```csharp
// ✅ 好:在 Dispose 中取消订阅
public class MyComponent : ComponentBase
{
public override Task InitializeAsync()
{
Settings.SettingChanged += OnSettingChanged;
return Task.CompletedTask;
}
public override void Dispose()
{
Settings.SettingChanged -= OnSettingChanged;
base.Dispose();
}
}
// ❌ 差:忘记取消订阅(内存泄漏)
public class MyComponent : ComponentBase
{
public override Task InitializeAsync()
{
Settings.SettingChanged += OnSettingChanged;
return Task.CompletedTask;
}
// 没有 Dispose导致内存泄漏
}
```
## 设置迁移
### 版本升级时的设置迁移
```csharp
public class MyPlugin : IPlugin
{
public async Task InitializeAsync(IPluginContext context)
{
var settings = context.Settings;
// 检查设置版本
var settingsVersion = settings.GetValue("SettingsVersion", 1);
if (settingsVersion < 2)
{
// 迁移到版本 2
MigrateToV2(settings);
settings.SetValue("SettingsVersion", 2);
}
if (settingsVersion < 3)
{
// 迁移到版本 3
MigrateToV3(settings);
settings.SetValue("SettingsVersion", 3);
}
}
private void MigrateToV2(ISettingsService settings)
{
// 例如:重命名设置键
if (settings.Contains("OldKey"))
{
var value = settings.GetValue<string>("OldKey", "");
settings.SetValue("NewKey", value);
settings.Remove("OldKey");
}
}
private void MigrateToV3(ISettingsService settings)
{
// 例如:更改数据格式
var oldFormat = settings.GetValue<string>("Location", "");
var newFormat = new LocationConfig
{
City = oldFormat,
Country = "中国"
};
settings.SetValue("LocationConfig", newFormat);
settings.Remove("Location");
}
}
```
## 下一步
- [主题与外观](04-主题外观.md) - 适配主题系统
- [插件通信](05-插件通信.md) - 插件间协作
- [设置 API 详解](../03-API参考/04-设置API.md) - API 参考文档
- [创建设置页](../04-实战案例/04-开发设置页.md) - 实战案例