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

790 lines
20 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.
# 组件系统详解
本文档详细介绍阑山桌面的桌面组件Widget系统包括组件架构、生命周期、渲染机制和最佳实践。
## 什么是桌面组件?
**桌面组件Widget** 是显示在桌面上的可视化模块,提供信息展示和快捷操作功能。
### 组件特点
- 🎨 **固定在桌面** - 显示在桌面图层,不会被普通窗口遮挡
- 🔄 **实时更新** - 定时刷新数据,保持信息最新
- ⚙️ **可配置** - 用户可以自定义组件行为和外观
- 🖱️ **可交互** - 支持点击、拖拽等用户操作
- 📐 **可布局** - 用户可以自由调整位置和大小
### 典型组件示例
| 组件类型 | 功能 | 更新频率 |
|---------|------|---------|
| **时钟组件** | 显示当前时间和日期 | 1秒 |
| **天气组件** | 显示天气信息 | 5-15分钟 |
| **日历组件** | 显示日程和待办 | 1小时 |
| **系统监控** | CPU、内存使用率 | 2秒 |
| **倒计时** | 重要日期倒计时 | 1秒 |
## 组件架构
### 组件三层结构
```
┌────────────────────────────────────────┐
│ Component (组件模型) │
│ ┌──────────────────────────────────┐ │
│ │ 业务逻辑 │ │
│ │ - 数据获取 │ │
│ │ - 状态管理 │ │
│ │ - 设置持久化 │ │
│ └──────────────────────────────────┘ │
└────────────────┬───────────────────────┘
│ 数据绑定
┌────────────────▼───────────────────────┐
│ ViewModel (视图模型) │
│ ┌──────────────────────────────────┐ │
│ │ 展示逻辑 │ │
│ │ - 属性通知 │ │
│ │ - 命令处理 │ │
│ │ - 数据格式化 │ │
│ └──────────────────────────────────┘ │
└────────────────┬───────────────────────┘
│ UI 绑定
┌────────────────▼───────────────────────┐
│ View (视图) │
│ ┌──────────────────────────────────┐ │
│ │ UI 渲染 │ │
│ │ - Avalonia AXAML │ │
│ │ - 样式和主题 │ │
│ │ - 用户交互 │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
```
### 组件基类层次
```
object
ObservableObject (MVVM Toolkit)
ComponentBase (Plugin SDK)
YourComponent (你的组件)
```
## 创建组件
### 步骤 1: 定义组件类
```csharp
using LanMountainDesktop.PluginSdk.Components;
using LanMountainDesktop.Shared.Contracts.Components;
using System.ComponentModel;
namespace MyPlugin.Components;
/// <summary>
/// 天气组件 - 显示当前天气信息
/// </summary>
[Component(
Id = "com.example.myplugin.weather",
Name = "天气",
Description = "显示当前天气和温度",
Category = "信息",
Icon = "avares://MyPlugin/Assets/weather-icon.png",
DefaultWidth = 200,
DefaultHeight = 150
)]
public class WeatherComponent : ComponentBase
{
// 组件唯一标识
public override string Id => "com.example.myplugin.weather";
// 组件显示名称
public override string Name => "天气";
// === 数据属性 ===
private string _location = "北京";
private double _temperature = 0;
private string _condition = "晴";
private string _icon = "☀️";
/// <summary>
/// 位置
/// </summary>
public string Location
{
get => _location;
set => SetProperty(ref _location, value);
}
/// <summary>
/// 温度(摄氏度)
/// </summary>
public double Temperature
{
get => _temperature;
set => SetProperty(ref _temperature, value);
}
/// <summary>
/// 天气状况
/// </summary>
public string Condition
{
get => _condition;
set => SetProperty(ref _condition, value);
}
/// <summary>
/// 天气图标
/// </summary>
public string Icon
{
get => _icon;
set => SetProperty(ref _icon, value);
}
// === 配置属性 ===
private bool _useFahrenheit = false;
/// <summary>
/// 是否使用华氏度
/// </summary>
public bool UseFahrenheit
{
get => _useFahrenheit;
set
{
if (SetProperty(ref _useFahrenheit, value))
{
// 保存到设置
Settings.SetValue("UseFahrenheit", value);
// 触发更新
OnPropertyChanged(nameof(DisplayTemperature));
}
}
}
/// <summary>
/// 显示温度(根据单位)
/// </summary>
public string DisplayTemperature
{
get
{
if (UseFahrenheit)
{
var fahrenheit = Temperature * 9 / 5 + 32;
return $"{fahrenheit:F1}°F";
}
return $"{Temperature:F1}°C";
}
}
// === 生命周期方法 ===
/// <summary>
/// 组件初始化
/// </summary>
public override async Task InitializeAsync()
{
// 从设置加载配置
Location = Settings.GetValue("Location", "北京");
UseFahrenheit = Settings.GetValue("UseFahrenheit", false);
// 首次加载数据
await FetchWeatherDataAsync();
Logger.LogInformation($"WeatherComponent initialized for {Location}");
}
/// <summary>
/// 组件定时更新
/// </summary>
public override async Task UpdateAsync()
{
// 每 10 分钟更新一次天气数据
var lastUpdate = Settings.GetValue<DateTime>("LastUpdate", DateTime.MinValue);
if (DateTime.Now - lastUpdate > TimeSpan.FromMinutes(10))
{
await FetchWeatherDataAsync();
}
}
/// <summary>
/// 组件销毁
/// </summary>
public override void Dispose()
{
// 清理资源
base.Dispose();
}
// === 业务逻辑 ===
private HttpClient? _httpClient;
private async Task FetchWeatherDataAsync()
{
try
{
_httpClient ??= new HttpClient();
// 调用天气 API
var url = $"https://api.weather.com/data?city={Location}";
var response = await _httpClient.GetStringAsync(url);
// 解析数据
var weatherData = ParseWeatherData(response);
// 更新属性
Temperature = weatherData.Temperature;
Condition = weatherData.Condition;
Icon = GetWeatherIcon(weatherData.Condition);
// 记录更新时间
Settings.SetValue("LastUpdate", DateTime.Now);
Logger.LogInformation($"Weather data updated for {Location}");
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to fetch weather data");
Condition = "加载失败";
}
}
private WeatherData ParseWeatherData(string json)
{
// 解析 JSON 数据
// 实际项目中使用 System.Text.Json 或 Newtonsoft.Json
return new WeatherData
{
Temperature = 25.5,
Condition = "晴"
};
}
private string GetWeatherIcon(string condition)
{
return condition switch
{
"晴" => "☀️",
"多云" => "⛅",
"阴" => "☁️",
"雨" => "🌧️",
"雪" => "❄️",
_ => "🌤️"
};
}
private class WeatherData
{
public double Temperature { get; set; }
public string Condition { get; set; } = "";
}
}
```
### 步骤 2: 创建视图
创建 `Views/WeatherComponentView.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.Views.WeatherComponentView"
x:DataType="vm:WeatherComponentViewModel">
<!-- 组件容器 -->
<Border Background="{DynamicResource CardBackgroundBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="16"
BorderBrush="{DynamicResource CardBorderBrush}"
BorderThickness="1"
BoxShadow="0 2 8 0 #20000000">
<Grid RowDefinitions="Auto,*,Auto">
<!-- 标题栏 -->
<StackPanel Grid.Row="0"
Orientation="Horizontal"
Spacing="8"
Margin="0,0,0,12">
<TextBlock Text="📍" FontSize="16" />
<TextBlock Text="{Binding Component.Location}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel>
<!-- 主要内容 -->
<StackPanel Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<!-- 天气图标 -->
<TextBlock Text="{Binding Component.Icon}"
FontSize="48"
HorizontalAlignment="Center" />
<!-- 温度 -->
<TextBlock Text="{Binding Component.DisplayTemperature}"
FontSize="32"
FontWeight="Bold"
HorizontalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<!-- 天气状况 -->
<TextBlock Text="{Binding Component.Condition}"
FontSize="16"
HorizontalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
<!-- 底部操作 -->
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="8"
Margin="0,12,0,0">
<!-- 刷新按钮 -->
<Button Command="{Binding RefreshCommand}"
Padding="8,4"
ToolTip.Tip="刷新">
<TextBlock Text="🔄" FontSize="14" />
</Button>
<!-- 设置按钮 -->
<Button Command="{Binding SettingsCommand}"
Padding="8,4"
ToolTip.Tip="设置">
<TextBlock Text="⚙️" FontSize="14" />
</Button>
</StackPanel>
</Grid>
</Border>
</UserControl>
```
代码后台 `WeatherComponentView.axaml.cs`
```csharp
using Avalonia.Controls;
namespace MyPlugin.Views;
public partial class WeatherComponentView : UserControl
{
public WeatherComponentView()
{
InitializeComponent();
}
}
```
### 步骤 3: 创建视图模型
创建 `ViewModels/WeatherComponentViewModel.cs`
```csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MyPlugin.Components;
namespace MyPlugin.ViewModels;
/// <summary>
/// 天气组件视图模型
/// </summary>
public partial class WeatherComponentViewModel : ObservableObject
{
[ObservableProperty]
private WeatherComponent _component;
public WeatherComponentViewModel(WeatherComponent component)
{
_component = component;
}
/// <summary>
/// 刷新命令
/// </summary>
[RelayCommand]
private async Task RefreshAsync()
{
// 强制刷新天气数据
await Component.UpdateAsync();
}
/// <summary>
/// 设置命令
/// </summary>
[RelayCommand]
private void Settings()
{
// 打开组件设置对话框
// 实际实现需要调用宿主的对话框服务
Component.Logger.LogInformation("Settings clicked");
}
}
```
### 步骤 4: 注册组件
在插件入口注册组件:
```csharp
public class Plugin : IPlugin
{
public async Task InitializeAsync(IPluginContext context)
{
var componentRegistry = context.Services
.GetService<IComponentRegistry>();
if (componentRegistry != null)
{
// 注册天气组件
componentRegistry.RegisterComponent<WeatherComponent>(
componentFactory: () => new WeatherComponent(),
viewFactory: (component) => new WeatherComponentView
{
DataContext = new WeatherComponentViewModel(
(WeatherComponent)component
)
}
);
context.Logger.LogInformation("WeatherComponent registered");
}
}
}
```
## ComponentBase API
### 核心属性
```csharp
public abstract class ComponentBase : ObservableObject, IComponent
{
// === 标识属性 ===
/// <summary>
/// 组件唯一标识符
/// </summary>
public abstract string Id { get; }
/// <summary>
/// 组件显示名称
/// </summary>
public abstract string Name { get; }
// === 服务访问 ===
/// <summary>
/// 日志记录器
/// </summary>
protected ILogger Logger { get; }
/// <summary>
/// 设置服务
/// </summary>
protected IComponentSettings Settings { get; }
/// <summary>
/// 服务提供者
/// </summary>
protected IServiceProvider Services { get; }
// === 生命周期方法 ===
/// <summary>
/// 组件初始化(创建时调用一次)
/// </summary>
public virtual Task InitializeAsync() => Task.CompletedTask;
/// <summary>
/// 组件更新定时调用默认1秒
/// </summary>
public virtual Task UpdateAsync() => Task.CompletedTask;
/// <summary>
/// 组件销毁(清理资源)
/// </summary>
public virtual void Dispose() { }
}
```
### 辅助方法
```csharp
/// <summary>
/// 设置属性值并触发通知
/// </summary>
protected bool SetProperty<T>(
ref T field,
T value,
[CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// 触发属性变更通知
/// </summary>
protected void OnPropertyChanged(
[CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(propertyName)
);
}
```
## 组件生命周期
### 完整生命周期
```
1. 用户添加组件
2. ComponentRegistry.CreateInstance()
├─ 调用 componentFactory()
├─ 创建组件实例
└─ 注入依赖Logger, Settings, Services
3. 调用 InitializeAsync()
├─ 加载设置
├─ 初始化数据
└─ 订阅事件
4. ComponentRegistry.CreateView()
├─ 调用 viewFactory()
├─ 创建视图
└─ 设置 DataContext
5. 添加到桌面
├─ 包装到 DesktopWidgetWindow
├─ 设置位置和大小
└─ 显示窗口
6. 定时更新循环
├─ 每 1 秒(可配置)
├─ 调用 UpdateAsync()
└─ UI 自动刷新(数据绑定)
7. 用户移除组件 / 应用关闭
8. 调用 Dispose()
├─ 取消订阅
├─ 保存状态
└─ 释放资源
9. 从桌面移除
└─ 关闭窗口
```
### 更新频率控制
```csharp
public class MyComponent : ComponentBase
{
private DateTime _lastUpdate;
private readonly TimeSpan _updateInterval = TimeSpan.FromMinutes(5);
public override async Task UpdateAsync()
{
// 控制更新频率
if (DateTime.Now - _lastUpdate < _updateInterval)
return;
await FetchDataAsync();
_lastUpdate = DateTime.Now;
}
}
```
## 组件设置
### 使用设置服务
```csharp
public class MyComponent : ComponentBase
{
public override Task InitializeAsync()
{
// 读取设置(带默认值)
var city = Settings.GetValue("City", "北京");
var refreshRate = Settings.GetValue("RefreshRate", 10);
var enabled = Settings.GetValue("Enabled", true);
// 读取复杂对象
var config = Settings.GetValue<MyConfig>("Config", new MyConfig());
return Task.CompletedTask;
}
public void SaveCity(string city)
{
// 保存设置
Settings.SetValue("City", city);
}
}
```
### 监听设置变更
```csharp
public class MyComponent : ComponentBase
{
public override Task InitializeAsync()
{
// 监听设置变更
Settings.SettingChanged += OnSettingChanged;
return Task.CompletedTask;
}
private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
{
if (e.Key == "City")
{
var newCity = e.NewValue as string;
// 响应城市变更
_ = FetchWeatherForCity(newCity);
}
}
public override void Dispose()
{
// 取消订阅
Settings.SettingChanged -= OnSettingChanged;
base.Dispose();
}
}
```
## 最佳实践
### ✅ 性能优化
```csharp
// ✅ 好:使用缓存
private string? _cachedData;
private DateTime _cacheTime;
public async Task<string> GetDataAsync()
{
if (_cachedData != null &&
DateTime.Now - _cacheTime < TimeSpan.FromMinutes(5))
{
return _cachedData;
}
_cachedData = await FetchDataAsync();
_cacheTime = DateTime.Now;
return _cachedData;
}
// ❌ 差:每次都重新获取
public async Task<string> GetDataAsync()
{
return await FetchDataAsync(); // 浪费资源
}
```
### ✅ 异步编程
```csharp
// ✅ 好:使用 async/await
public override async Task UpdateAsync()
{
await FetchDataAsync();
}
// ❌ 差:阻塞线程
public override Task UpdateAsync()
{
FetchDataAsync().Wait(); // 阻塞!
return Task.CompletedTask;
}
```
### ✅ 错误处理
```csharp
// ✅ 好:捕获并记录异常
public override async Task UpdateAsync()
{
try
{
await FetchDataAsync();
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "Network error");
DisplayError("网络错误");
}
catch (Exception ex)
{
Logger.LogError(ex, "Unexpected error");
DisplayError("未知错误");
}
}
// ❌ 差:忽略异常
public override async Task UpdateAsync()
{
await FetchDataAsync(); // 异常会传播到宿主
}
```
### ✅ 资源管理
```csharp
// ✅ 好:正确释放资源
public class MyComponent : ComponentBase
{
private HttpClient? _httpClient;
private CancellationTokenSource? _cts;
public override void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
_httpClient?.Dispose();
base.Dispose();
}
}
// ❌ 差:不释放资源
public class MyComponent : ComponentBase
{
private HttpClient _httpClient = new(); // 内存泄漏
}
```
## 下一步
- [设置系统](03-设置系统.md) - 管理组件配置
- [主题与外观](04-主题外观.md) - 适配主题
- [ComponentBase API](../03-API参考/03-组件API.md) - API 详细文档
- [天气组件案例](../04-实战案例/01-天气组件.md) - 完整实战