mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-26 03:44:25 +08:00
feat.新增了插件开发文档
This commit is contained in:
398
docs/Plugins develop/02-核心概念与原理/01-插件生命周期.md
Normal file
398
docs/Plugins develop/02-核心概念与原理/01-插件生命周期.md
Normal file
@@ -0,0 +1,398 @@
|
||||
# 01-插件生命周期
|
||||
|
||||
理解插件的生命周期,是开发稳定可靠插件的基础。本文详细讲解插件从加载到卸载的完整过程。
|
||||
|
||||
---
|
||||
|
||||
## 🔄 生命周期概览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 阑山桌面启动 │
|
||||
└───────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. 发现插件 │
|
||||
│ - 扫描插件目录 │
|
||||
│ - 解析 plugin.json │
|
||||
│ - 验证 API 版本兼容性 │
|
||||
└───────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2. 加载插件 │
|
||||
│ - 创建 AssemblyLoadContext │
|
||||
│ - 加载插件 DLL │
|
||||
│ - 查找入口类(带 [PluginEntrance] 特性) │
|
||||
└───────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 3. 初始化(Initialize) │
|
||||
│ - 调用 Plugin.Initialize() │
|
||||
│ - 注册组件、设置页面、服务 │
|
||||
│ - ⚠️ 此时 UI 尚未完全就绪 │
|
||||
└───────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 4. 运行中 │
|
||||
│ - 组件被添加到桌面 │
|
||||
│ - 用户与组件交互 │
|
||||
│ - 设置页面被打开 │
|
||||
└───────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 5. 停用/卸载 │
|
||||
│ - 用户禁用插件 │
|
||||
│ - 或关闭阑山桌面 │
|
||||
│ - 释放资源(当前版本无显式卸载回调) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 各阶段详解
|
||||
|
||||
### 阶段 1:发现插件
|
||||
|
||||
**时机:** 阑山桌面启动时
|
||||
|
||||
**过程:**
|
||||
1. 扫描 `%LOCALAPPDATA%\LanMountainDesktop\plugins\` 目录
|
||||
2. 读取每个 `.laapp` 包中的 `plugin.json`
|
||||
3. 验证 `apiVersion` 是否与宿主兼容
|
||||
4. 检查 `id` 是否唯一
|
||||
|
||||
**可能失败的原因:**
|
||||
- `plugin.json` 格式错误
|
||||
- `apiVersion` 不兼容
|
||||
- `id` 与其他插件冲突
|
||||
|
||||
---
|
||||
|
||||
### 阶段 2:加载插件
|
||||
|
||||
**时机:** 发现成功后
|
||||
|
||||
**过程:**
|
||||
1. 创建独立的 `AssemblyLoadContext`
|
||||
2. 加载插件 DLL 及其依赖项
|
||||
3. 查找带有 `[PluginEntrance]` 特性的类
|
||||
4. 实例化插件入口类
|
||||
|
||||
**代码示例:**
|
||||
```csharp
|
||||
[PluginEntrance] // ← 这个特性标记入口类
|
||||
public sealed class Plugin : PluginBase
|
||||
{
|
||||
// 插件实例在此阶段被创建
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **重要:** 此阶段**不要**执行耗时操作,只应进行简单的字段初始化。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 3:初始化(Initialize)
|
||||
|
||||
**时机:** 插件加载完成后
|
||||
|
||||
**这是插件开发中最重要的阶段!**
|
||||
|
||||
#### 方法签名
|
||||
|
||||
```csharp
|
||||
public override void Initialize(
|
||||
HostBuilderContext context, // 宿主构建上下文
|
||||
IServiceCollection services) // 服务注册集合
|
||||
```
|
||||
|
||||
#### 可执行的操作
|
||||
|
||||
✅ **可以做的:**
|
||||
- 注册桌面组件
|
||||
- 注册设置页面
|
||||
- 注册服务到依赖注入容器
|
||||
- 读取配置
|
||||
- 初始化资源
|
||||
|
||||
❌ **不应该做的:**
|
||||
- 访问 UI 元素(UI 尚未就绪)
|
||||
- 执行耗时阻塞操作
|
||||
- 创建窗口或对话框
|
||||
|
||||
#### 典型初始化代码
|
||||
|
||||
```csharp
|
||||
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
// 1. 注册服务
|
||||
services.AddSingleton<IWeatherService, WeatherService>();
|
||||
|
||||
// 2. 注册桌面组件
|
||||
services.AddPluginDesktopComponent<WeatherWidget>(
|
||||
new PluginDesktopComponentOptions
|
||||
{
|
||||
ComponentId = "MyPlugin.Weather",
|
||||
DisplayName = "天气",
|
||||
IconKey = "Weather",
|
||||
Category = "工具",
|
||||
MinWidthCells = 4,
|
||||
MinHeightCells = 3
|
||||
});
|
||||
|
||||
// 3. 注册设置页面
|
||||
services.AddPluginSettingsSection(
|
||||
"myplugin-settings",
|
||||
"天气设置",
|
||||
section => section
|
||||
.AddToggle("auto_refresh", "自动刷新", defaultValue: true)
|
||||
.AddNumber("interval", "刷新间隔(分钟)", defaultValue: 30),
|
||||
iconKey: "Settings");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 4:运行中
|
||||
|
||||
**时机:** 初始化完成后,直到插件被禁用或宿主关闭
|
||||
|
||||
**特点:**
|
||||
- 组件可以被添加到桌面
|
||||
- 用户可以与组件交互
|
||||
- 设置页面可以被打开
|
||||
- 定时器可以运行
|
||||
|
||||
#### 组件生命周期
|
||||
|
||||
```
|
||||
用户添加组件
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 创建组件实例 │ ← 调用构造函数
|
||||
│ (Dependency │ 注入 IServiceProvider
|
||||
│ Injection) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 组件初始化 │ ← 可在此时加载数据
|
||||
│ (Loaded事件) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 渲染显示 │ ← 用户看到组件
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
▼ ▼
|
||||
用户交互 定时更新
|
||||
│ │
|
||||
└────┬────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 组件移除 │ ← 用户删除组件
|
||||
│ (Unloaded事件) │ 或关闭宿主
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 5:停用/卸载
|
||||
|
||||
**时机:**
|
||||
- 用户在设置中禁用插件
|
||||
- 卸载插件
|
||||
- 关闭阑山桌面
|
||||
|
||||
**当前限制:**
|
||||
- SDK v4 暂无显式的卸载回调方法
|
||||
- 资源释放依赖 .NET 垃圾回收
|
||||
- 建议:
|
||||
- 使用 `IDisposable` 模式管理资源
|
||||
- 在组件卸载事件中清理资源
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ 启动时序图
|
||||
|
||||
```
|
||||
阑山桌面 插件系统 你的插件
|
||||
│ │ │
|
||||
│── 启动 ───────►│ │
|
||||
│ │ │
|
||||
│ │── 发现插件 ───►│
|
||||
│ │ │ (读取 plugin.json)
|
||||
│ │◄───────────────│
|
||||
│ │ │
|
||||
│ │── 加载 DLL ───►│
|
||||
│ │ │ (AssemblyLoadContext)
|
||||
│ │◄───────────────│
|
||||
│ │ │
|
||||
│ │── 创建实例 ───►│
|
||||
│ │ │ (调用构造函数)
|
||||
│ │◄───────────────│
|
||||
│ │ │
|
||||
│ │── Initialize ─►│
|
||||
│ │ │ (注册组件/服务)
|
||||
│ │◄───────────────│
|
||||
│ │ │
|
||||
│◄───────────────│ │
|
||||
│ │ │
|
||||
│── UI就绪 ─────►│ │
|
||||
│ │ │
|
||||
│ │── 用户添加组件 ─►│
|
||||
│ │ │ (创建组件实例)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 1. Initialize 方法保持轻量
|
||||
|
||||
```csharp
|
||||
// ✅ 好的做法:快速注册
|
||||
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IDataService, DataService>();
|
||||
services.AddPluginDesktopComponent<MyWidget>(options);
|
||||
}
|
||||
|
||||
// ❌ 避免:耗时操作
|
||||
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
// 不要这样做!
|
||||
var data = FetchDataFromInternet().Result; // 阻塞!
|
||||
services.AddSingleton(data);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 延迟加载数据
|
||||
|
||||
```csharp
|
||||
public class MyWidget : Border
|
||||
{
|
||||
private readonly IDataService _dataService;
|
||||
|
||||
public MyWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
_dataService = context.ServiceProvider.GetRequiredService<IDataService>();
|
||||
|
||||
// 在 Loaded 事件中加载数据,而不是构造函数
|
||||
Loaded += async (_, _) =>
|
||||
{
|
||||
await LoadDataAsync();
|
||||
};
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
var data = await _dataService.GetDataAsync();
|
||||
// 更新 UI
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 正确处理资源释放
|
||||
|
||||
```csharp
|
||||
public class MyWidget : Border, IDisposable
|
||||
{
|
||||
private readonly Timer _timer;
|
||||
private bool _disposed;
|
||||
|
||||
public MyWidget()
|
||||
{
|
||||
_timer = new Timer(OnTimerTick, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
|
||||
|
||||
// 在卸载时释放资源
|
||||
Unloaded += (_, _) => Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
_timer?.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免循环依赖
|
||||
|
||||
```csharp
|
||||
// ❌ 避免:服务之间相互依赖
|
||||
public class ServiceA
|
||||
{
|
||||
public ServiceA(ServiceB b) { } // 循环依赖风险
|
||||
}
|
||||
|
||||
public class ServiceB
|
||||
{
|
||||
public ServiceB(ServiceA a) { }
|
||||
}
|
||||
|
||||
// ✅ 好的做法:使用接口解耦
|
||||
public class ServiceA
|
||||
{
|
||||
public ServiceA(IServiceB b) { }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 问题 1:Initialize 中访问 UI 报错
|
||||
|
||||
**现象:** `InvalidOperationException` 或空引用
|
||||
|
||||
**原因:** Initialize 在 UI 就绪前调用
|
||||
|
||||
**解决:** 延迟到组件创建后再访问 UI
|
||||
|
||||
### 问题 2:服务注册顺序问题
|
||||
|
||||
**现象:** 依赖注入找不到服务
|
||||
|
||||
**原因:** 服务注册顺序不正确
|
||||
|
||||
**解决:** 先注册服务,再注册依赖这些服务的组件
|
||||
|
||||
### 问题 3:插件加载慢
|
||||
|
||||
**现象:** 宿主启动变慢
|
||||
|
||||
**原因:** Initialize 中执行耗时操作
|
||||
|
||||
**解决:** 将耗时操作移到后台线程或延迟执行
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- [PluginBase 源码](../../LanMountainDesktop.PluginSdk/PluginBase.cs)
|
||||
- [IPlugin 接口](../../LanMountainDesktop.PluginSdk/IPlugin.cs)
|
||||
- [02-桌面组件系统](02-桌面组件系统.md)
|
||||
- [03-设置系统集成](03-设置系统集成.md)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
理解生命周期后,学习如何创建桌面组件:
|
||||
|
||||
👉 **[02-桌面组件系统](02-桌面组件系统.md)** - 创建可视化组件
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026年4月*
|
||||
393
docs/Plugins develop/02-核心概念与原理/02-桌面组件系统.md
Normal file
393
docs/Plugins develop/02-核心概念与原理/02-桌面组件系统.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# 02-桌面组件系统
|
||||
|
||||
桌面组件(Desktop Component)是阑山桌面插件的核心功能。本文详细讲解组件系统的工作原理和开发方法。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 什么是桌面组件
|
||||
|
||||
桌面组件是显示在阑山桌面上的可视化元素,用户可以自由:
|
||||
- 添加/删除组件
|
||||
- 拖动调整位置
|
||||
- 调整大小
|
||||
- 配置属性
|
||||
|
||||
**常见组件示例:**
|
||||
- 时钟组件 - 显示当前时间
|
||||
- 天气组件 - 显示天气信息
|
||||
- 日历组件 - 显示日期和日程
|
||||
- 系统监控 - 显示 CPU/内存使用率
|
||||
|
||||
---
|
||||
|
||||
## 📐 网格系统
|
||||
|
||||
阑山桌面使用网格系统管理组件布局。
|
||||
|
||||
### 网格概念
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
|
||||
│ │ 2x2│ │ 2x2│ │ 2x2│ │ 2x2│ │
|
||||
│ │ 格 │ │ 格 │ │ 格 │ │ 格 │ │
|
||||
│ └────┘ └────┘ └────┘ └────┘ │
|
||||
│ ┌────┐ ┌────────┐ ┌────┐ │
|
||||
│ │ 2x2│ │ 4x2 │ │ 2x2│ │
|
||||
│ │ 格 │ │ 格 │ │ 格 │ │
|
||||
│ └────┘ └────────┘ └────┘ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ 4x3 │ │ 4x3 │ │
|
||||
│ │ 格 │ │ 格 │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
每格大小:约 60-80 像素(根据 DPI 自动调整)
|
||||
```
|
||||
|
||||
### 组件尺寸
|
||||
|
||||
组件尺寸以**格数**为单位:
|
||||
|
||||
| 属性 | 说明 | 示例 |
|
||||
|-----|------|------|
|
||||
| `MinWidthCells` | 最小宽度(格数) | 4 = 4格宽 |
|
||||
| `MinHeightCells` | 最小高度(格数) | 3 = 3格高 |
|
||||
|
||||
**常见尺寸参考:**
|
||||
|
||||
| 组件类型 | 推荐尺寸 | 实际像素(约) |
|
||||
|---------|---------|--------------|
|
||||
| 小图标 | 2x2 | 120x120 |
|
||||
| 天气卡片 | 4x3 | 240x180 |
|
||||
| 时钟 | 4x4 | 240x240 |
|
||||
| 日历 | 6x4 | 360x240 |
|
||||
| 宽面板 | 8x3 | 480x180 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 创建组件
|
||||
|
||||
### 步骤 1:创建组件类
|
||||
|
||||
组件是继承自 Avalonia 控件的类:
|
||||
|
||||
```csharp
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace MyPlugin;
|
||||
|
||||
public class WeatherWidget : Border // 继承自 Border 或其他控件
|
||||
{
|
||||
public WeatherWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 组件初始化
|
||||
InitializeComponent(context);
|
||||
}
|
||||
|
||||
private void InitializeComponent(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 设置背景
|
||||
Background = new SolidColorBrush(Colors.Transparent);
|
||||
|
||||
// 设置圆角(使用宿主主题)
|
||||
CornerRadius = context.Appearance.ResolveCornerRadius(
|
||||
PluginCornerRadiusPreset.Component);
|
||||
|
||||
// 创建内容
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Text = "天气组件",
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
Child = textBlock;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2:注册组件
|
||||
|
||||
在 `Plugin.Initialize` 中注册:
|
||||
|
||||
```csharp
|
||||
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
services.AddPluginDesktopComponent<WeatherWidget>(
|
||||
new PluginDesktopComponentOptions
|
||||
{
|
||||
ComponentId = "MyPlugin.Weather", // 唯一标识
|
||||
DisplayName = "天气", // 显示名称
|
||||
IconKey = "Weather", // 图标(Fluent 图标名)
|
||||
Category = "工具", // 分类
|
||||
MinWidthCells = 4, // 最小宽度(格)
|
||||
MinHeightCells = 3, // 最小高度(格)
|
||||
CornerRadiusPreset = PluginCornerRadiusPreset.Component // 圆角预设
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### PluginDesktopComponentOptions 详解
|
||||
|
||||
| 属性 | 必需 | 说明 | 示例 |
|
||||
|-----|------|------|------|
|
||||
| `ComponentId` | ✅ | 唯一标识符 | `"MyPlugin.Weather"` |
|
||||
| `DisplayName` | ✅ | 显示名称 | `"天气"` |
|
||||
| `IconKey` | ✅ | 图标键名 | `"Weather"`、`"Clock"` |
|
||||
| `Category` | ✅ | 分类 | `"工具"`、`"信息"` |
|
||||
| `MinWidthCells` | ✅ | 最小宽度(格) | `4` |
|
||||
| `MinHeightCells` | ✅ | 最小高度(格) | `3` |
|
||||
| `CornerRadiusPreset` | ❌ | 圆角预设 | `PluginCornerRadiusPreset.Component` |
|
||||
| `ResizeMode` | ❌ | 调整大小模式 | `PluginDesktopComponentResizeMode.Free` |
|
||||
|
||||
### 常用 Fluent 图标
|
||||
|
||||
| 图标键名 | 用途 |
|
||||
|---------|------|
|
||||
| `Weather` | 天气相关 |
|
||||
| `Clock` | 时钟、时间 |
|
||||
| `Calendar` | 日历、日期 |
|
||||
| `Settings` | 设置 |
|
||||
| `Home` | 主页 |
|
||||
| `Search` | 搜索 |
|
||||
| `Star` | 收藏 |
|
||||
| `Heart` | 喜欢 |
|
||||
| `Info` | 信息 |
|
||||
| `Warning` | 警告 |
|
||||
|
||||
完整图标列表:[Fluent UI System Icons](https://github.com/microsoft/fluentui-system-icons)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 组件外观
|
||||
|
||||
### 圆角设置
|
||||
|
||||
插件必须使用宿主提供的圆角系统,以保持视觉一致性:
|
||||
|
||||
```csharp
|
||||
public WeatherWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 获取组件标准圆角
|
||||
var cornerRadius = context.Appearance.ResolveCornerRadius(
|
||||
PluginCornerRadiusPreset.Component);
|
||||
|
||||
CornerRadius = cornerRadius;
|
||||
}
|
||||
```
|
||||
|
||||
**可用的圆角预设:**
|
||||
|
||||
| 预设 | 用途 |
|
||||
|-----|------|
|
||||
| `Micro` | 微小元素 |
|
||||
| `Xs` | 小元素 |
|
||||
| `Sm` | 小卡片 |
|
||||
| `Md` | 普通按钮/卡片 |
|
||||
| `Lg` | 大面板 |
|
||||
| `Xl` | 强调容器 |
|
||||
| `Component` | **桌面组件标准** |
|
||||
| `Default` | 自适应 |
|
||||
|
||||
### 背景与透明
|
||||
|
||||
```csharp
|
||||
// 透明背景(推荐,让宿主壁纸透出)
|
||||
Background = new SolidColorBrush(Colors.Transparent);
|
||||
|
||||
// 毛玻璃效果
|
||||
Background = new SolidColorBrush(Color.Parse("#40FFFFFF"));
|
||||
|
||||
// 纯色背景
|
||||
Background = new SolidColorBrush(Color.Parse("#FF2D2D2D"));
|
||||
```
|
||||
|
||||
### 响应主题变化
|
||||
|
||||
```csharp
|
||||
public WeatherWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 订阅主题变化
|
||||
context.Appearance.AppearanceChanged += (_, _) =>
|
||||
{
|
||||
UpdateAppearance();
|
||||
};
|
||||
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
// 根据当前主题更新颜色
|
||||
var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
|
||||
|
||||
Foreground = new SolidColorBrush(isDark ? Colors.White : Colors.Black);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📏 尺寸与布局
|
||||
|
||||
### 获取实际尺寸
|
||||
|
||||
```csharp
|
||||
public WeatherWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 订阅尺寸变化
|
||||
SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
// 获取当前实际尺寸(像素)
|
||||
var width = Bounds.Width;
|
||||
var height = Bounds.Height;
|
||||
|
||||
// 根据尺寸调整内容
|
||||
if (width < 200)
|
||||
{
|
||||
// 小尺寸模式
|
||||
ShowCompactView();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 完整模式
|
||||
ShowFullView();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自适应布局
|
||||
|
||||
```csharp
|
||||
private void UpdateLayout()
|
||||
{
|
||||
var width = Bounds.Width;
|
||||
var height = Bounds.Height;
|
||||
|
||||
// 根据宽高比调整布局
|
||||
if (width > height * 2)
|
||||
{
|
||||
// 宽屏模式 - 水平排列
|
||||
_layout.Orientation = Orientation.Horizontal;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 正常模式 - 垂直排列
|
||||
_layout.Orientation = Orientation.Vertical;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 组件生命周期事件
|
||||
|
||||
```csharp
|
||||
public WeatherWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 组件加载完成(此时已添加到视觉树)
|
||||
Loaded += OnLoaded;
|
||||
|
||||
// 组件卸载(用户删除或关闭宿主)
|
||||
Unloaded += OnUnloaded;
|
||||
|
||||
// 尺寸变化
|
||||
SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
private async void OnLoaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// 加载数据
|
||||
await LoadDataAsync();
|
||||
|
||||
// 启动定时器
|
||||
_timer = new Timer(OnTimerTick, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
private void OnUnloaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// 清理资源
|
||||
_timer?.Dispose();
|
||||
_httpClient?.Dispose();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💾 组件设置持久化
|
||||
|
||||
组件可以保存自己的设置:
|
||||
|
||||
```csharp
|
||||
public class WeatherWidget : Border
|
||||
{
|
||||
private readonly IComponentSettingsAccessor _settings;
|
||||
|
||||
public WeatherWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 获取设置访问器
|
||||
_settings = context.Settings;
|
||||
|
||||
// 读取设置
|
||||
var city = _settings.GetValue<string>("city", defaultValue: "北京");
|
||||
var autoRefresh = _settings.GetValue<bool>("auto_refresh", defaultValue: true);
|
||||
|
||||
// 保存设置
|
||||
_settings.SetValue("city", "上海");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 问题 1:组件不显示在库中
|
||||
|
||||
**排查:**
|
||||
1. 确认已调用 `AddPluginDesktopComponent`
|
||||
2. 检查 `ComponentId` 是否唯一
|
||||
3. 确认组件类是 `public`
|
||||
|
||||
### 问题 2:组件显示异常
|
||||
|
||||
**排查:**
|
||||
1. 检查构造函数参数是否正确(需要 `PluginDesktopComponentContext`)
|
||||
2. 确认没有抛出未处理异常
|
||||
3. 查看日志文件
|
||||
|
||||
### 问题 3:圆角不生效
|
||||
|
||||
**原因:** 插件无法访问宿主 XAML 资源
|
||||
|
||||
**解决:** 使用代码设置圆角(见上文)
|
||||
|
||||
### 问题 4:尺寸不正确
|
||||
|
||||
**排查:**
|
||||
1. 检查 `MinWidthCells` 和 `MinHeightCells` 设置
|
||||
2. 确认内容没有强制尺寸
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- [PluginDesktopComponentOptions 源码](../../LanMountainDesktop.PluginSdk/PluginDesktopComponentOptions.cs)
|
||||
- [04-外观与主题系统](04-外观与主题系统.md)
|
||||
- [01-开发天气组件](../04-实战案例/01-开发天气组件.md)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
学习如何添加设置页面:
|
||||
|
||||
👉 **[03-设置系统集成](03-设置系统集成.md)** - 让用户配置你的组件
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026年4月*
|
||||
353
docs/Plugins develop/02-核心概念与原理/03-设置系统集成.md
Normal file
353
docs/Plugins develop/02-核心概念与原理/03-设置系统集成.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# 03-设置系统集成
|
||||
|
||||
设置系统允许插件在阑山桌面的设置窗口中添加自己的配置页面,让用户可以自定义插件行为。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 设置系统概述
|
||||
|
||||
阑山桌面提供两种设置页面模式:
|
||||
|
||||
| 模式 | 适用场景 | 复杂度 | 灵活性 |
|
||||
|-----|---------|--------|--------|
|
||||
| **声明式设置** | 简单的键值配置 | 低 | 中 |
|
||||
| **自定义设置页** | 复杂交互、自定义控件 | 中 | 高 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 声明式设置
|
||||
|
||||
通过链式 API 声明配置项,宿主自动生成设置页面。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```csharp
|
||||
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
services.AddPluginSettingsSection(
|
||||
sectionId: "myplugin-settings", // 设置节唯一标识
|
||||
displayName: "我的插件设置", // 显示名称
|
||||
configure: section => // 配置设置项
|
||||
{
|
||||
section
|
||||
.AddToggle("enabled", "启用插件", defaultValue: true)
|
||||
.AddText("api_key", "API密钥", defaultValue: "")
|
||||
.AddNumber("interval", "刷新间隔(秒)", defaultValue: 60, minimum: 10, maximum: 3600)
|
||||
.AddSelect("theme", "主题", new[]
|
||||
{
|
||||
new SettingsOptionChoice("light", "浅色"),
|
||||
new SettingsOptionChoice("dark", "深色"),
|
||||
new SettingsOptionChoice("auto", "跟随系统")
|
||||
}, defaultValue: "auto");
|
||||
},
|
||||
iconKey: "Settings"); // 图标
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的设置类型
|
||||
|
||||
| 方法 | 类型 | 用途 | 示例 |
|
||||
|-----|------|------|------|
|
||||
| `AddToggle` | 布尔 | 开关选项 | 启用/禁用 |
|
||||
| `AddText` | 字符串 | 文本输入 | API密钥、用户名 |
|
||||
| `AddNumber` | 数值 | 数字输入 | 刷新间隔、数量 |
|
||||
| `AddSelect` | 枚举 | 下拉选择 | 主题、语言 |
|
||||
| `AddPath` | 路径 | 文件/文件夹选择 | 保存路径 |
|
||||
| `AddList` | 列表 | 字符串列表 | 服务器地址列表 |
|
||||
|
||||
### 各类型详解
|
||||
|
||||
#### Toggle(开关)
|
||||
|
||||
```csharp
|
||||
.AddToggle(
|
||||
key: "auto_update", // 设置键
|
||||
displayName: "自动更新", // 显示名称
|
||||
defaultValue: true, // 默认值
|
||||
description: "启动时检查更新" // 可选描述
|
||||
)
|
||||
```
|
||||
|
||||
#### Text(文本)
|
||||
|
||||
```csharp
|
||||
.AddText(
|
||||
key: "api_key",
|
||||
displayName: "API密钥",
|
||||
defaultValue: "",
|
||||
placeholder: "请输入API密钥", // 占位符
|
||||
isPassword: true // 密码输入(掩码显示)
|
||||
)
|
||||
```
|
||||
|
||||
#### Number(数值)
|
||||
|
||||
```csharp
|
||||
.AddNumber(
|
||||
key: "refresh_interval",
|
||||
displayName: "刷新间隔",
|
||||
defaultValue: 60,
|
||||
minimum: 10, // 最小值
|
||||
maximum: 3600, // 最大值
|
||||
increment: 10 // 步进值
|
||||
)
|
||||
```
|
||||
|
||||
#### Select(选择)
|
||||
|
||||
```csharp
|
||||
.AddSelect(
|
||||
key: "display_mode",
|
||||
displayName: "显示模式",
|
||||
choices: new[]
|
||||
{
|
||||
new SettingsOptionChoice("compact", "紧凑"),
|
||||
new SettingsOptionChoice("normal", "标准"),
|
||||
new SettingsOptionChoice("detailed", "详细")
|
||||
},
|
||||
defaultValue: "normal"
|
||||
)
|
||||
```
|
||||
|
||||
#### Path(路径)
|
||||
|
||||
```csharp
|
||||
.AddPath(
|
||||
key: "save_location",
|
||||
displayName: "保存位置",
|
||||
defaultValue: "",
|
||||
pathType: SettingsPathType.Folder, // Folder 或 File
|
||||
dialogTitle: "选择保存文件夹"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 自定义设置页
|
||||
|
||||
当声明式设置无法满足需求时,可以创建自定义设置页面。
|
||||
|
||||
### 步骤 1:创建设置页类
|
||||
|
||||
```csharp
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
|
||||
namespace MyPlugin;
|
||||
|
||||
public class MySettingsPage : SettingsPageBase
|
||||
{
|
||||
private readonly IPluginSettingsService _settingsService;
|
||||
|
||||
public MySettingsPage(IPluginSettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
// 创建页面内容
|
||||
var panel = new StackPanel { Spacing = 16 };
|
||||
|
||||
// 添加自定义控件
|
||||
var expander = new SettingsExpander
|
||||
{
|
||||
Header = "高级设置",
|
||||
Description = "配置插件的高级选项"
|
||||
};
|
||||
|
||||
var toggle = new ToggleSwitch
|
||||
{
|
||||
Content = "启用实验性功能"
|
||||
};
|
||||
|
||||
expander.Items.Add(toggle);
|
||||
panel.Children.Add(expander);
|
||||
|
||||
// 添加颜色选择器示例
|
||||
var colorPicker = new ColorPicker
|
||||
{
|
||||
Header = "主题颜色"
|
||||
};
|
||||
panel.Children.Add(colorPicker);
|
||||
|
||||
Content = panel;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2:注册自定义设置页
|
||||
|
||||
```csharp
|
||||
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
services.AddPluginSettingsSection<MySettingsPage>(
|
||||
sectionId: "myplugin-advanced",
|
||||
displayName: "高级设置",
|
||||
iconKey: "Settings");
|
||||
}
|
||||
```
|
||||
|
||||
### 混合模式
|
||||
|
||||
可以同时使用声明式设置和自定义视图:
|
||||
|
||||
```csharp
|
||||
services.AddPluginSettingsSection(
|
||||
sectionId: "myplugin-settings",
|
||||
displayName: "插件设置",
|
||||
configure: section => section
|
||||
.SetCustomView<MyCustomSettingsPage>() // 设置自定义视图
|
||||
.AddToggle("enabled", "启用") // 同时声明设置项
|
||||
.AddText("api_key", "API密钥"),
|
||||
iconKey: "Settings");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💾 读取和保存设置
|
||||
|
||||
### 在服务中读取设置
|
||||
|
||||
```csharp
|
||||
public class WeatherService
|
||||
{
|
||||
private readonly IPluginSettingsService _settings;
|
||||
|
||||
public WeatherService(IPluginSettingsService settings)
|
||||
{
|
||||
_settings = settings;
|
||||
|
||||
// 读取设置
|
||||
var apiKey = _settings.GetValue<string>("api_key", "");
|
||||
var autoRefresh = _settings.GetValue<bool>("auto_update", true);
|
||||
var interval = _settings.GetValue<int>("refresh_interval", 60);
|
||||
|
||||
// 监听设置变化
|
||||
_settings.SettingsChanged += (sender, e) =>
|
||||
{
|
||||
if (e.Key == "refresh_interval")
|
||||
{
|
||||
UpdateTimerInterval(e.NewValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在组件中读取设置
|
||||
|
||||
```csharp
|
||||
public class WeatherWidget : Border
|
||||
{
|
||||
public WeatherWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 通过 context 获取设置
|
||||
var settings = context.Settings;
|
||||
|
||||
var city = settings.GetValue<string>("city", "北京");
|
||||
var unit = settings.GetValue<string>("temperature_unit", "celsius");
|
||||
|
||||
// 监听设置变化
|
||||
context.Settings.SettingsChanged += (_, e) =>
|
||||
{
|
||||
if (e.Key == "city")
|
||||
{
|
||||
RefreshWeather(e.NewValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 保存设置
|
||||
|
||||
```csharp
|
||||
// 在设置页面中保存
|
||||
private void SaveButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_settingsService.SetValue("api_key", ApiKeyTextBox.Text);
|
||||
_settingsService.SetValue("auto_update", AutoUpdateToggle.IsChecked ?? false);
|
||||
|
||||
// 设置会自动持久化,无需手动保存文件
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔔 设置变更通知
|
||||
|
||||
### 订阅变更事件
|
||||
|
||||
```csharp
|
||||
public class MyService
|
||||
{
|
||||
public MyService(IPluginSettingsService settings)
|
||||
{
|
||||
settings.SettingsChanged += OnSettingsChanged;
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
|
||||
{
|
||||
Console.WriteLine($"设置变更: {e.Key}");
|
||||
Console.WriteLine($"旧值: {e.OldValue}");
|
||||
Console.WriteLine($"新值: {e.NewValue}");
|
||||
|
||||
// 根据变更的键执行相应操作
|
||||
switch (e.Key)
|
||||
{
|
||||
case "refresh_interval":
|
||||
UpdateRefreshTimer((int)e.NewValue);
|
||||
break;
|
||||
case "theme":
|
||||
ApplyTheme((string)e.NewValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 问题 1:设置不保存
|
||||
|
||||
**排查:**
|
||||
1. 确认 `sectionId` 唯一且合法
|
||||
2. 检查设置键名是否正确
|
||||
3. 查看日志是否有权限错误
|
||||
|
||||
### 问题 2:设置页面不显示
|
||||
|
||||
**排查:**
|
||||
1. 确认已调用 `AddPluginSettingsSection`
|
||||
2. 检查 `sectionId` 是否唯一
|
||||
3. 确认设置页类是 `public`
|
||||
|
||||
### 问题 3:设置变更通知不触发
|
||||
|
||||
**原因:** 可能订阅的是不同实例
|
||||
|
||||
**解决:** 确保使用注入的 `IPluginSettingsService`
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- [IPluginSettingsService 源码](../../LanMountainDesktop.PluginSdk/IPluginSettingsService.cs)
|
||||
- [SettingsPageBase 源码](../../LanMountainDesktop.PluginSdk/SettingsPageBase.cs)
|
||||
- [04-开发设置页面](../04-实战案例/04-开发设置页面.md)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
学习外观系统:
|
||||
|
||||
👉 **[04-外观与主题系统](04-外观与主题系统.md)** - 适配宿主主题
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026年4月*
|
||||
308
docs/Plugins develop/02-核心概念与原理/04-外观与主题系统.md
Normal file
308
docs/Plugins develop/02-核心概念与原理/04-外观与主题系统.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# 04-外观与主题系统
|
||||
|
||||
阑山桌面支持暗色/浅色主题切换,插件需要适配宿主的视觉风格,保持界面一致性。
|
||||
|
||||
---
|
||||
|
||||
## 🎨 主题系统概述
|
||||
|
||||
阑山桌面使用 Avalonia UI 的主题系统,支持:
|
||||
|
||||
- **浅色主题** - 明亮背景,深色文字
|
||||
- **深色主题** - 深色背景,浅色文字
|
||||
- **跟随系统** - 自动匹配 Windows/macOS 主题
|
||||
|
||||
---
|
||||
|
||||
## 🌗 检测当前主题
|
||||
|
||||
### 在组件中检测
|
||||
|
||||
```csharp
|
||||
using Avalonia;
|
||||
using Avalonia.Styling;
|
||||
|
||||
public class MyWidget : Border
|
||||
{
|
||||
public MyWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 检测当前主题
|
||||
var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
|
||||
|
||||
// 根据主题设置颜色
|
||||
UpdateTheme(isDark);
|
||||
|
||||
// 监听主题变化
|
||||
if (Application.Current != null)
|
||||
{
|
||||
Application.Current.ActualThemeVariantChanged += (_, _) =>
|
||||
{
|
||||
var newIsDark = Application.Current.ActualThemeVariant == ThemeVariant.Dark;
|
||||
UpdateTheme(newIsDark);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTheme(bool isDark)
|
||||
{
|
||||
Background = new SolidColorBrush(
|
||||
isDark ? Color.Parse("#FF1E1E1E") : Color.Parse("#FFFFFFFF"));
|
||||
|
||||
Foreground = new SolidColorBrush(
|
||||
isDark ? Colors.White : Colors.Black);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📐 圆角系统
|
||||
|
||||
插件必须使用宿主提供的圆角系统,确保与内置组件视觉一致。
|
||||
|
||||
### 为什么插件不能使用 XAML 资源
|
||||
|
||||
插件运行在独立的 `AssemblyLoadContext` 中,无法直接访问宿主的资源字典。因此 `{DynamicResource DesignCornerRadiusComponent}` 在插件 XAML 中无效。
|
||||
|
||||
### 使用代码设置圆角
|
||||
|
||||
```csharp
|
||||
public class MyWidget : Border
|
||||
{
|
||||
public MyWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 方法 1:使用预设(推荐)
|
||||
CornerRadius = context.Appearance.ResolveCornerRadius(
|
||||
PluginCornerRadiusPreset.Component);
|
||||
|
||||
// 方法 2:带最小/最大值限制
|
||||
CornerRadius = context.Appearance.ResolveCornerRadius(
|
||||
PluginCornerRadiusPreset.Component,
|
||||
minimum: new CornerRadius(8),
|
||||
maximum: new CornerRadius(24));
|
||||
|
||||
// 方法 3:自定义基础值,应用全局缩放
|
||||
CornerRadius = context.Appearance.ResolveScaledCornerRadius(
|
||||
baseRadius: 16,
|
||||
minimum: 8,
|
||||
maximum: 32);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 圆角预设
|
||||
|
||||
| 预设 | 默认值 | 用途 |
|
||||
|-----|-------|------|
|
||||
| `Micro` | 6px | 微小元素 |
|
||||
| `Xs` | 12px | 小元素、图标容器 |
|
||||
| `Sm` | 14px | 小卡片 |
|
||||
| `Md` | 20px | 普通按钮/卡片 |
|
||||
| `Lg` | 28px | 大面板 |
|
||||
| `Xl` | 32px | 强调容器 |
|
||||
| `Island` | 36px | 大型容器 |
|
||||
| `Component` | 18px | **桌面组件标准** |
|
||||
| `Default` | 自适应 | 根据尺寸自动计算 |
|
||||
|
||||
### 内部元素圆角
|
||||
|
||||
组件内部的卡片、按钮应使用更小的圆角:
|
||||
|
||||
```csharp
|
||||
// 组件根容器 - 使用 Component 预设
|
||||
CornerRadius = context.ResolveCornerRadius(PluginCornerRadiusPreset.Component);
|
||||
|
||||
// 内部卡片 - 使用 Md 预设
|
||||
var innerCard = new Border
|
||||
{
|
||||
CornerRadius = context.ResolveCornerRadius(PluginCornerRadiusPreset.Md),
|
||||
Background = new SolidColorBrush(Colors.LightGray)
|
||||
};
|
||||
|
||||
// 按钮 - 使用 Sm 预设
|
||||
var button = new Button
|
||||
{
|
||||
CornerRadius = context.ResolveCornerRadius(PluginCornerRadiusPreset.Sm)
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 颜色系统
|
||||
|
||||
### 推荐的颜色策略
|
||||
|
||||
```csharp
|
||||
// 透明背景(推荐)- 让宿主壁纸透出
|
||||
Background = new SolidColorBrush(Colors.Transparent);
|
||||
|
||||
// 毛玻璃效果
|
||||
Background = new SolidColorBrush(Color.Parse(isDark ? "#40FFFFFF" : "#40000000"));
|
||||
|
||||
// 卡片背景
|
||||
Background = new SolidColorBrush(Color.Parse(isDark ? "#FF2D2D2D" : "#FFFFFFFF"));
|
||||
|
||||
// 强调色(使用系统强调色)
|
||||
var accentColor = Color.Parse("#FF0078D4"); // 阑山桌面主色调
|
||||
```
|
||||
|
||||
### 文字颜色
|
||||
|
||||
```csharp
|
||||
// 主要文字
|
||||
Foreground = new SolidColorBrush(isDark ? Colors.White : Colors.Black);
|
||||
|
||||
// 次要文字
|
||||
Foreground = new SolidColorBrush(isDark ? Color.Parse("#FFCCCCCC") : Color.Parse("#FF666666"));
|
||||
|
||||
// 禁用文字
|
||||
Foreground = new SolidColorBrush(isDark ? Color.Parse("#FF666666") : Color.Parse("#FF999999"));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 响应外观变化
|
||||
|
||||
### 订阅外观变化事件
|
||||
|
||||
```csharp
|
||||
public class MyWidget : Border
|
||||
{
|
||||
public MyWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 订阅外观变化
|
||||
context.Appearance.AppearanceChanged += (_, _) =>
|
||||
{
|
||||
UpdateAppearance();
|
||||
};
|
||||
|
||||
// 初始化
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
// 重新应用圆角(用户可能调整了全局圆角设置)
|
||||
var context = ...; // 获取 context
|
||||
CornerRadius = context.Appearance.ResolveCornerRadius(
|
||||
PluginCornerRadiusPreset.Component);
|
||||
|
||||
// 更新主题颜色
|
||||
var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
|
||||
UpdateThemeColors(isDark);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧩 使用 FluentAvalonia 控件
|
||||
|
||||
推荐使用 FluentAvalonia 控件库,它们自动适配主题:
|
||||
|
||||
```xml
|
||||
<Window xmlns:ui="using:FluentAvalonia.UI.Controls">
|
||||
<ui:SettingsExpander Header="设置项">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<ui:FontIconSource Glyph="" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
</ui:SettingsExpander>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### 常用 FluentAvalonia 控件
|
||||
|
||||
| 控件 | 用途 |
|
||||
|-----|------|
|
||||
| `SettingsExpander` | 设置项展开器 |
|
||||
| `SettingsCard` | 设置卡片 |
|
||||
| `ColorPicker` | 颜色选择器 |
|
||||
| `NumberBox` | 数字输入框 |
|
||||
| `ToggleSwitch` | 开关 |
|
||||
|
||||
---
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 1. 始终使用透明背景
|
||||
|
||||
```csharp
|
||||
// ✅ 好的做法
|
||||
Background = new SolidColorBrush(Colors.Transparent);
|
||||
|
||||
// ❌ 避免硬编码背景色
|
||||
Background = new SolidColorBrush(Colors.White);
|
||||
```
|
||||
|
||||
### 2. 组件根容器必须使用 Component 圆角
|
||||
|
||||
```csharp
|
||||
// ✅ 正确
|
||||
CornerRadius = context.ResolveCornerRadius(PluginCornerRadiusPreset.Component);
|
||||
|
||||
// ❌ 错误 - 硬编码
|
||||
CornerRadius = new CornerRadius(18);
|
||||
```
|
||||
|
||||
### 3. 响应主题变化
|
||||
|
||||
```csharp
|
||||
// ✅ 订阅变化事件
|
||||
Application.Current.ActualThemeVariantChanged += OnThemeChanged;
|
||||
|
||||
// ❌ 只在构造函数中设置一次
|
||||
```
|
||||
|
||||
### 4. 使用语义化颜色
|
||||
|
||||
```csharp
|
||||
// ✅ 根据用途选择颜色
|
||||
var primaryText = isDark ? Colors.White : Colors.Black;
|
||||
var secondaryText = isDark ? Color.Parse("#FFCCCCCC") : Color.Parse("#FF666666");
|
||||
|
||||
// ❌ 避免随意使用颜色
|
||||
var textColor = Color.Parse("#FF123456");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 问题 1:圆角不生效
|
||||
|
||||
**原因:** 在 XAML 中使用 `{DynamicResource}`
|
||||
|
||||
**解决:** 在代码中设置圆角(见上文)
|
||||
|
||||
### 问题 2:主题切换后颜色不对
|
||||
|
||||
**原因:** 没有订阅主题变化事件
|
||||
|
||||
**解决:** 添加 `ActualThemeVariantChanged` 事件处理
|
||||
|
||||
### 问题 3:组件与内置组件风格不一致
|
||||
|
||||
**排查:**
|
||||
1. 检查圆角是否使用 `PluginCornerRadiusPreset.Component`
|
||||
2. 检查背景是否透明
|
||||
3. 检查是否使用了 FluentAvalonia 控件
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- [CORNER_RADIUS_SPEC.md](../../docs/CORNER_RADIUS_SPEC.md)
|
||||
- [VISUAL_SPEC.md](../../docs/VISUAL_SPEC.md)
|
||||
- [FluentAvalonia 文档](https://github.com/amwx/FluentAvalonia)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
学习插件间通信:
|
||||
|
||||
👉 **[05-插件间通信](05-插件间通信.md)** - 与其他插件协作
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026年4月*
|
||||
375
docs/Plugins develop/02-核心概念与原理/05-插件间通信.md
Normal file
375
docs/Plugins develop/02-核心概念与原理/05-插件间通信.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# 05-插件间通信
|
||||
|
||||
插件之间可以通过消息总线和服务导出进行通信,实现功能协作和数据共享。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 通信方式概述
|
||||
|
||||
| 方式 | 适用场景 | 方向 |
|
||||
|-----|---------|------|
|
||||
| **消息总线** | 事件通知、广播 | 多对多 |
|
||||
| **服务导出** | 功能共享、API 暴露 | 一对多 |
|
||||
| **共享契约** | 数据交换 | 双向 |
|
||||
|
||||
---
|
||||
|
||||
## 📢 消息总线
|
||||
|
||||
使用 `IPluginMessageBus` 发布和订阅消息。
|
||||
|
||||
### 发布消息
|
||||
|
||||
```csharp
|
||||
public class WeatherService
|
||||
{
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
|
||||
public WeatherService(IPluginMessageBus messageBus)
|
||||
{
|
||||
_messageBus = messageBus;
|
||||
}
|
||||
|
||||
public async Task UpdateWeatherAsync()
|
||||
{
|
||||
var weather = await FetchWeatherAsync();
|
||||
|
||||
// 发布天气更新消息
|
||||
_messageBus.Publish(new WeatherUpdatedMessage
|
||||
{
|
||||
City = weather.City,
|
||||
Temperature = weather.Temperature,
|
||||
Condition = weather.Condition
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 定义消息
|
||||
public class WeatherUpdatedMessage
|
||||
{
|
||||
public string City { get; set; } = "";
|
||||
public double Temperature { get; set; }
|
||||
public string Condition { get; set; } = "";
|
||||
}
|
||||
```
|
||||
|
||||
### 订阅消息
|
||||
|
||||
```csharp
|
||||
public class AnotherPluginService
|
||||
{
|
||||
public AnotherPluginService(IPluginMessageBus messageBus)
|
||||
{
|
||||
// 订阅天气更新消息
|
||||
messageBus.Subscribe<WeatherUpdatedMessage>(OnWeatherUpdated);
|
||||
}
|
||||
|
||||
private void OnWeatherUpdated(WeatherUpdatedMessage message)
|
||||
{
|
||||
Console.WriteLine($"收到天气更新: {message.City} {message.Temperature}°C");
|
||||
|
||||
// 根据天气更新自己的状态
|
||||
UpdateDisplay(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 取消订阅
|
||||
|
||||
```csharp
|
||||
public class MyService : IDisposable
|
||||
{
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
private readonly Guid _subscriptionId;
|
||||
|
||||
public MyService(IPluginMessageBus messageBus)
|
||||
{
|
||||
_messageBus = messageBus;
|
||||
_subscriptionId = messageBus.Subscribe<WeatherUpdatedMessage>(OnWeatherUpdated);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// 取消订阅,避免内存泄漏
|
||||
_messageBus.Unsubscribe<WeatherUpdatedMessage>(_subscriptionId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 服务导出
|
||||
|
||||
插件可以将服务导出,供其他插件使用。
|
||||
|
||||
### 导出服务
|
||||
|
||||
```csharp
|
||||
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
// 注册服务
|
||||
services.AddSingleton<IWeatherService, WeatherService>();
|
||||
|
||||
// 导出服务供其他插件使用
|
||||
services.AddPluginServiceExport<IWeatherService>(
|
||||
serviceKey: "MyPlugin.WeatherService",
|
||||
description: "提供天气查询服务");
|
||||
}
|
||||
|
||||
// 定义服务接口
|
||||
public interface IWeatherService
|
||||
{
|
||||
Task<WeatherInfo> GetCurrentWeatherAsync(string city);
|
||||
Task<List<WeatherForecast>> GetForecastAsync(string city, int days);
|
||||
}
|
||||
|
||||
// 实现服务
|
||||
public class WeatherService : IWeatherService
|
||||
{
|
||||
public async Task<WeatherInfo> GetCurrentWeatherAsync(string city)
|
||||
{
|
||||
// 实现天气查询
|
||||
}
|
||||
|
||||
public async Task<List<WeatherForecast>> GetForecastAsync(string city, int days)
|
||||
{
|
||||
// 实现天气预报
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用其他插件的服务
|
||||
|
||||
```csharp
|
||||
public class MyWidget : Border
|
||||
{
|
||||
public MyWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
// 获取其他插件导出的服务
|
||||
var weatherService = context.ServiceProvider
|
||||
.GetExportedService<IWeatherService>("MyPlugin.WeatherService");
|
||||
|
||||
if (weatherService != null)
|
||||
{
|
||||
// 使用服务
|
||||
LoadWeatherAsync(weatherService);
|
||||
}
|
||||
}
|
||||
|
||||
private async void LoadWeatherAsync(IWeatherService weatherService)
|
||||
{
|
||||
var weather = await weatherService.GetCurrentWeatherAsync("北京");
|
||||
UpdateUI(weather);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 服务导出选项
|
||||
|
||||
```csharp
|
||||
services.AddPluginServiceExport<IWeatherService>(
|
||||
serviceKey: "MyPlugin.WeatherService",
|
||||
description: "提供天气查询服务",
|
||||
version: "1.0.0",
|
||||
isPublic: true); // 是否公开给其他插件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 共享契约
|
||||
|
||||
通过 `sharedContracts` 在插件间共享类型定义。
|
||||
|
||||
### 定义共享契约
|
||||
|
||||
```csharp
|
||||
// 在共享类库项目中定义
|
||||
namespace MyPlugin.Shared;
|
||||
|
||||
public interface IWeatherData
|
||||
{
|
||||
string City { get; }
|
||||
double Temperature { get; }
|
||||
string Condition { get; }
|
||||
}
|
||||
|
||||
public class WeatherData : IWeatherData
|
||||
{
|
||||
public string City { get; set; } = "";
|
||||
public double Temperature { get; set; }
|
||||
public string Condition { get; set; } = "";
|
||||
}
|
||||
```
|
||||
|
||||
### 在 plugin.json 中声明
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.example.weather",
|
||||
"name": "天气插件",
|
||||
"sharedContracts": [
|
||||
"MyPlugin.Shared.IWeatherData",
|
||||
"MyPlugin.Shared.WeatherData"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 使用共享类型
|
||||
|
||||
```csharp
|
||||
// 插件 A 发布数据
|
||||
public class WeatherService
|
||||
{
|
||||
public WeatherData GetWeather()
|
||||
{
|
||||
return new WeatherData
|
||||
{
|
||||
City = "北京",
|
||||
Temperature = 25.5,
|
||||
Condition = "晴"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 插件 B 接收数据
|
||||
public class ConsumerService
|
||||
{
|
||||
public void ProcessWeather(IWeatherData weather)
|
||||
{
|
||||
Console.WriteLine($"{weather.City}: {weather.Temperature}°C");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 安全考虑
|
||||
|
||||
### 服务导出安全
|
||||
|
||||
```csharp
|
||||
// 只导出必要的接口,不暴露实现细节
|
||||
public interface IPublicApi
|
||||
{
|
||||
Task<Data> GetDataAsync();
|
||||
}
|
||||
|
||||
internal class InternalService : IPublicApi
|
||||
{
|
||||
// 内部实现细节不暴露
|
||||
private readonly SecretKey _key;
|
||||
|
||||
public async Task<Data> GetDataAsync()
|
||||
{
|
||||
// 实现
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 消息验证
|
||||
|
||||
```csharp
|
||||
private void OnMessageReceived(MyMessage message)
|
||||
{
|
||||
// 验证消息来源
|
||||
if (message.SenderId != "TrustedPlugin")
|
||||
{
|
||||
return; // 忽略不信任来源的消息
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 1. 使用接口定义服务契约
|
||||
|
||||
```csharp
|
||||
// ✅ 好的做法 - 定义接口
|
||||
public interface IWeatherService { }
|
||||
|
||||
// ❌ 避免 - 直接导出实现类
|
||||
services.AddPluginServiceExport<WeatherService>(...);
|
||||
```
|
||||
|
||||
### 2. 处理服务不可用情况
|
||||
|
||||
```csharp
|
||||
// ✅ 优雅处理服务缺失
|
||||
var service = context.ServiceProvider
|
||||
.GetExportedService<IWeatherService>("key");
|
||||
|
||||
if (service == null)
|
||||
{
|
||||
// 显示提示或降级处理
|
||||
ShowServiceUnavailableMessage();
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 及时取消消息订阅
|
||||
|
||||
```csharp
|
||||
// ✅ 在 Dispose 中取消订阅
|
||||
public void Dispose()
|
||||
{
|
||||
_messageBus.Unsubscribe<MyMessage>(_subscriptionId);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 版本兼容性
|
||||
|
||||
```csharp
|
||||
// 在服务导出中包含版本信息
|
||||
services.AddPluginServiceExport<IWeatherService>(
|
||||
serviceKey: "MyPlugin.WeatherService",
|
||||
version: "2.0.0", // 语义化版本
|
||||
description: "天气服务 v2");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 问题 1:消息收不到
|
||||
|
||||
**排查:**
|
||||
1. 确认消息类型完全一致(包括命名空间)
|
||||
2. 检查订阅是否在消息发布之前
|
||||
3. 确认没有取消订阅
|
||||
|
||||
### 问题 2:服务找不到
|
||||
|
||||
**排查:**
|
||||
1. 确认服务已导出(`AddPluginServiceExport`)
|
||||
2. 检查 `serviceKey` 是否正确
|
||||
3. 确认依赖的插件已安装并启用
|
||||
|
||||
### 问题 3:类型转换错误
|
||||
|
||||
**原因:** 共享契约类型不匹配
|
||||
|
||||
**解决:** 确保所有插件使用相同版本的共享契约程序集
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- [IPluginMessageBus 源码](../../LanMountainDesktop.PluginSdk/IPluginMessageBus.cs)
|
||||
- [IPluginExportRegistry 源码](../../LanMountainDesktop.PluginSdk/IPluginExportRegistry.cs)
|
||||
- [Shared.Contracts](../../LanMountainDesktop.Shared.Contracts/)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
查看实战案例:
|
||||
|
||||
👉 **[01-开发天气组件](../04-实战案例/01-开发天气组件.md)** - 完整插件开发流程
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026年4月*
|
||||
Reference in New Issue
Block a user