mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
20 KiB
20 KiB
组件系统详解
本文档详细介绍阑山桌面的桌面组件(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: 定义组件类
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:
<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:
using Avalonia.Controls;
namespace MyPlugin.Views;
public partial class WeatherComponentView : UserControl
{
public WeatherComponentView()
{
InitializeComponent();
}
}
步骤 3: 创建视图模型
创建 ViewModels/WeatherComponentViewModel.cs:
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: 注册组件
在插件入口注册组件:
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
核心属性
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() { }
}
辅助方法
/// <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. 从桌面移除
└─ 关闭窗口
更新频率控制
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;
}
}
组件设置
使用设置服务
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);
}
}
监听设置变更
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();
}
}
最佳实践
✅ 性能优化
// ✅ 好:使用缓存
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(); // 浪费资源
}
✅ 异步编程
// ✅ 好:使用 async/await
public override async Task UpdateAsync()
{
await FetchDataAsync();
}
// ❌ 差:阻塞线程
public override Task UpdateAsync()
{
FetchDataAsync().Wait(); // 阻塞!
return Task.CompletedTask;
}
✅ 错误处理
// ✅ 好:捕获并记录异常
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(); // 异常会传播到宿主
}
✅ 资源管理
// ✅ 好:正确释放资源
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(); // 内存泄漏
}
下一步
- 设置系统 - 管理组件配置
- 主题与外观 - 适配主题
- ComponentBase API - API 详细文档
- 天气组件案例 - 完整实战