From 8d1dbaea5421945f50099719f46fba1cc474420d Mon Sep 17 00:00:00 2001 From: lincube Date: Mon, 8 Jun 2026 12:18:58 +0800 Subject: [PATCH] =?UTF-8?q?feat.=E6=96=87=E6=A1=A3=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../01-快速开始/02-创建第一个插件.md | 530 +++++++++++ .../02-核心概念/01-插件生命周期.md | 683 ++++++++++++++ docs/01-插件开发/02-核心概念/02-组件系统.md | 789 ++++++++++++++++ docs/01-插件开发/02-核心概念/03-设置系统.md | 858 ++++++++++++++++++ docs/01-插件开发/03-API参考/01-IPlugin接口.md | 719 +++++++++++++++ .../03-API参考/02-IPluginContext.md | 717 +++++++++++++++ docs/FINAL_REPORT.md | 332 +++++++ docs/PROGRESS_REPORT.md | 213 +++++ 8 files changed, 4841 insertions(+) create mode 100644 docs/01-插件开发/01-快速开始/02-创建第一个插件.md create mode 100644 docs/01-插件开发/02-核心概念/01-插件生命周期.md create mode 100644 docs/01-插件开发/02-核心概念/02-组件系统.md create mode 100644 docs/01-插件开发/02-核心概念/03-设置系统.md create mode 100644 docs/01-插件开发/03-API参考/01-IPlugin接口.md create mode 100644 docs/01-插件开发/03-API参考/02-IPluginContext.md create mode 100644 docs/FINAL_REPORT.md create mode 100644 docs/PROGRESS_REPORT.md diff --git a/docs/01-插件开发/01-快速开始/02-创建第一个插件.md b/docs/01-插件开发/01-快速开始/02-创建第一个插件.md new file mode 100644 index 0000000..185d9af --- /dev/null +++ b/docs/01-插件开发/01-快速开始/02-创建第一个插件.md @@ -0,0 +1,530 @@ +# 创建第一个插件 + +通过这个教程,你将在 15 分钟内创建一个简单但功能完整的插件。 + +## 学习目标 + +- ✅ 使用模板创建插件项目 +- ✅ 实现插件入口类 +- ✅ 创建一个简单的桌面组件 +- ✅ 注册组件到宿主 +- ✅ 运行和测试插件 + +## 前置准备 + +确保你已经: +- ✅ 安装了 .NET 10 SDK +- ✅ 安装了插件模板(参考 [环境准备](01-环境准备.md)) +- ✅ 有一个支持 C# 的 IDE + +## 步骤 1: 创建项目 + +### 使用模板创建 + +```powershell +# 创建新插件项目 +dotnet new lmd-plugin -n HelloWorldPlugin + +# 进入项目目录 +cd HelloWorldPlugin + +# 还原依赖 +dotnet restore +``` + +### 项目结构预览 + +``` +HelloWorldPlugin/ +├── HelloWorldPlugin.csproj # 项目文件 +├── Plugin.cs # 插件入口(我们要修改这个) +├── plugin.json # 插件清单(我们要修改这个) +├── Components/ +│ └── SampleComponent.cs # 示例组件(我们要修改这个) +├── Views/ +│ └── SampleComponentView.axaml # 组件视图(我们要修改这个) +├── ViewModels/ +│ └── SampleComponentViewModel.cs +├── Assets/ +│ └── icon.png # 插件图标 +└── Settings/ + └── PluginSettingsPage.axaml # 设置页 +``` + +## 步骤 2: 配置插件清单 + +编辑 `plugin.json`,修改基本信息: + +```json +{ + "Id": "com.example.helloworldplugin", + "Name": "Hello World Plugin", + "Version": "1.0.0", + "Author": "Your Name", + "Description": "My first LanMountainDesktop plugin - displays a greeting", + "MinHostVersion": "1.0.0", + "SdkVersion": "5.0.0", + "Dependencies": [], + "Permissions": [], + "Icon": "Assets/icon.png", + "Homepage": "https://github.com/yourusername/helloworldplugin" +} +``` + +### 字段说明 + +- **Id**: 插件唯一标识符,建议使用反向域名格式 +- **Name**: 用户看到的插件名称 +- **Version**: 插件版本号(语义化版本) +- **MinHostVersion**: 最低支持的宿主版本 +- **SdkVersion**: 使用的 SDK 版本 + +## 步骤 3: 实现插件入口 + +编辑 `Plugin.cs`: + +```csharp +using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Shared.Contracts; +using HelloWorldPlugin.Components; +using HelloWorldPlugin.Views; +using HelloWorldPlugin.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace HelloWorldPlugin; + +/// +/// Hello World 插件入口 +/// +public class Plugin : IPlugin +{ + public string Id => "com.example.helloworldplugin"; + public string Name => "Hello World Plugin"; + public string Version => "1.0.0"; + + private IPluginContext? _context; + + /// + /// 插件初始化 + /// + public async Task InitializeAsync(IPluginContext context) + { + _context = context; + + // 记录日志 + context.Logger.LogInformation("Hello World Plugin is initializing..."); + + // 获取组件注册表 + var componentRegistry = context.Services + .GetService(); + + if (componentRegistry != null) + { + // 注册 Hello World 组件 + componentRegistry.RegisterComponent( + componentFactory: () => new HelloWorldComponent(), + viewFactory: (component) => new HelloWorldComponentView + { + DataContext = new HelloWorldComponentViewModel( + (HelloWorldComponent)component + ) + } + ); + + context.Logger.LogInformation( + "HelloWorldComponent registered successfully" + ); + } + + // 异步操作示例(如果需要) + await Task.CompletedTask; + } + + /// + /// 插件关闭 + /// + public async Task ShutdownAsync() + { + _context?.Logger.LogInformation( + "Hello World Plugin is shutting down..." + ); + + // 清理资源(如果有) + await Task.CompletedTask; + } +} +``` + +### 代码说明 + +1. **实现 IPlugin 接口** - 定义插件的基本信息和生命周期 +2. **InitializeAsync** - 插件加载时调用,注册组件和服务 +3. **ShutdownAsync** - 插件卸载时调用,清理资源 +4. **日志记录** - 使用 `context.Logger` 记录日志 + +## 步骤 4: 创建组件类 + +编辑 `Components/SampleComponent.cs`,重命名为 `HelloWorldComponent.cs`: + +```csharp +using LanMountainDesktop.PluginSdk.Components; +using LanMountainDesktop.Shared.Contracts.Components; +using System.ComponentModel; + +namespace HelloWorldPlugin.Components; + +/// +/// Hello World 桌面组件 +/// +[Component( + Id = "com.example.helloworldplugin.helloworld", + Name = "Hello World", + Description = "Displays a friendly greeting message", + Category = "Demo", + Icon = "avares://HelloWorldPlugin/Assets/icon.png" +)] +public class HelloWorldComponent : ComponentBase +{ + public override string Id => "com.example.helloworldplugin.helloworld"; + public override string Name => "Hello World"; + + private string _greeting = "Hello, World!"; + private int _clickCount = 0; + + /// + /// 问候语 + /// + public string Greeting + { + get => _greeting; + set => SetProperty(ref _greeting, value); + } + + /// + /// 点击次数 + /// + public int ClickCount + { + get => _clickCount; + set => SetProperty(ref _clickCount, value); + } + + /// + /// 组件初始化 + /// + public override Task InitializeAsync() + { + // 从设置加载问候语 + Greeting = Settings.GetValue("Greeting", "Hello, World!"); + ClickCount = Settings.GetValue("ClickCount", 0); + + Logger.LogInformation("HelloWorldComponent initialized"); + return Task.CompletedTask; + } + + /// + /// 组件更新(定时调用) + /// + public override Task UpdateAsync() + { + // 这里可以更新组件数据 + // 例如:从 API 获取数据、更新时间等 + return Task.CompletedTask; + } + + /// + /// 增加点击次数 + /// + public void IncrementClickCount() + { + ClickCount++; + Settings.SetValue("ClickCount", ClickCount); + } +} +``` + +### 组件说明 + +- **ComponentBase** - 所有组件的基类,提供基础功能 +- **属性通知** - 使用 `SetProperty` 自动触发 UI 更新 +- **设置持久化** - 使用 `Settings` 保存和读取配置 +- **InitializeAsync** - 组件创建时调用 +- **UpdateAsync** - 定时调用(默认 1 秒),用于更新数据 + +## 步骤 5: 创建组件视图 + +编辑 `Views/SampleComponentView.axaml`,重命名为 `HelloWorldComponentView.axaml`: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +代码后台 `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; + +/// +/// 天气组件视图模型 +/// +public partial class WeatherComponentViewModel : ObservableObject +{ + [ObservableProperty] + private WeatherComponent _component; + + public WeatherComponentViewModel(WeatherComponent component) + { + _component = component; + } + + /// + /// 刷新命令 + /// + [RelayCommand] + private async Task RefreshAsync() + { + // 强制刷新天气数据 + await Component.UpdateAsync(); + } + + /// + /// 设置命令 + /// + [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(); + + if (componentRegistry != null) + { + // 注册天气组件 + componentRegistry.RegisterComponent( + 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 +{ + // === 标识属性 === + + /// + /// 组件唯一标识符 + /// + public abstract string Id { get; } + + /// + /// 组件显示名称 + /// + public abstract string Name { get; } + + // === 服务访问 === + + /// + /// 日志记录器 + /// + protected ILogger Logger { get; } + + /// + /// 设置服务 + /// + protected IComponentSettings Settings { get; } + + /// + /// 服务提供者 + /// + protected IServiceProvider Services { get; } + + // === 生命周期方法 === + + /// + /// 组件初始化(创建时调用一次) + /// + public virtual Task InitializeAsync() => Task.CompletedTask; + + /// + /// 组件更新(定时调用,默认1秒) + /// + public virtual Task UpdateAsync() => Task.CompletedTask; + + /// + /// 组件销毁(清理资源) + /// + public virtual void Dispose() { } +} +``` + +### 辅助方法 + +```csharp +/// +/// 设置属性值并触发通知 +/// +protected bool SetProperty( + ref T field, + T value, + [CallerMemberName] string? propertyName = null) +{ + if (EqualityComparer.Default.Equals(field, value)) + return false; + + field = value; + OnPropertyChanged(propertyName); + return true; +} + +/// +/// 触发属性变更通知 +/// +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("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 GetDataAsync() +{ + if (_cachedData != null && + DateTime.Now - _cacheTime < TimeSpan.FromMinutes(5)) + { + return _cachedData; + } + + _cachedData = await FetchDataAsync(); + _cacheTime = DateTime.Now; + return _cachedData; +} + +// ❌ 差:每次都重新获取 +public async Task 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) - 完整实战 diff --git a/docs/01-插件开发/02-核心概念/03-设置系统.md b/docs/01-插件开发/02-核心概念/03-设置系统.md new file mode 100644 index 0000000..a24d9e0 --- /dev/null +++ b/docs/01-插件开发/02-核心概念/03-设置系统.md @@ -0,0 +1,858 @@ +# 设置系统 + +本文档介绍阑山桌面的设置系统,包括配置管理、持久化、设置页面和最佳实践。 + +## 设置系统概览 + +阑山桌面提供了统一的设置系统,用于管理应用、插件和组件的配置数据。 + +### 核心特性 + +- 💾 **自动持久化** - 设置自动保存到本地 +- 🔔 **变更通知** - 监听设置变更事件 +- 📁 **分域管理** - 按命名空间组织设置 +- 🔒 **类型安全** - 泛型 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("Config", new ComponentConfig()); + + return Task.CompletedTask; + } + + public void UpdateLocation(string location) + { + Location = location; + + // 保存设置 + Settings.SetValue("Location", location); + } +} +``` + +## 设置 API + +### ISettingsService 接口 + +```csharp +public interface ISettingsService +{ + /// + /// 获取设置值 + /// + T GetValue(string key, T defaultValue); + + /// + /// 设置值 + /// + void SetValue(string key, T value); + + /// + /// 删除设置 + /// + void Remove(string key); + + /// + /// 检查设置是否存在 + /// + bool Contains(string key); + + /// + /// 获取所有键 + /// + IEnumerable GetAllKeys(); + + /// + /// 清空所有设置 + /// + void Clear(); + + /// + /// 设置变更事件 + /// + event EventHandler? SettingChanged; +} +``` + +### 基本用法 + +```csharp +// 读取设置 +var value = settings.GetValue("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 FavoriteCities { get; set; } = new(); +} + +// 保存对象 +var config = new WeatherConfig +{ + City = "上海", + Unit = "Celsius", + RefreshInterval = 15, + FavoriteCities = new List { "北京", "上海", "广州" } +}; +settings.SetValue("WeatherConfig", config); + +// 读取对象 +var savedConfig = settings.GetValue( + "WeatherConfig", + new WeatherConfig() +); +``` + +### 集合类型 + +```csharp +// 列表 +var favoriteColors = new List { "红色", "蓝色", "绿色" }; +settings.SetValue("FavoriteColors", favoriteColors); +var colors = settings.GetValue>("FavoriteColors", new List()); + +// 字典 +var preferences = new Dictionary +{ + ["Language"] = "zh-CN", + ["Timezone"] = "Asia/Shanghai" +}; +settings.SetValue("Preferences", preferences); +var prefs = settings.GetValue>( + "Preferences", + new Dictionary() +); +``` + +## 监听设置变更 + +### 订阅变更事件 + +```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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +