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

20 KiB
Raw Blame History

组件系统详解

本文档详细介绍阑山桌面的桌面组件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(); // 内存泄漏
}

下一步