luolan.winland.Core
2.3.0
dotnet add package luolan.winland.Core --version 2.3.0
NuGet\Install-Package luolan.winland.Core -Version 2.3.0
<PackageReference Include="luolan.winland.Core" Version="2.3.0" />
<PackageVersion Include="luolan.winland.Core" Version="2.3.0" />
<PackageReference Include="luolan.winland.Core" />
paket add luolan.winland.Core --version 2.3.0
#r "nuget: luolan.winland.Core, 2.3.0"
#:package luolan.winland.Core@2.3.0
#addin nuget:?package=luolan.winland.Core&version=2.3.0
#tool nuget:?package=luolan.winland.Core&version=2.3.0
WinIsland 插件开发指南(SDK 2.0)
SDK 2.0 与 1.x 不兼容。 插件必须以
plugins/<id>/ + plugin.json(或.lwp包)提供,入口类型为IIslandPlugin; 1.x 的散装 DLL、IIslandModule、IslandPluginAttribute全部不再支持。迁移见文末。
1. 插件格式
plugins/
hw-monitor/ ← 一个插件一个目录,目录名建议等于插件 Id
plugin.json ← 清单(唯一元数据来源)
HardwareMonitor.dll ← 入口程序集(plugin.json 的 entry_dll)
HardwareMonitor.deps.json ← 依赖清单(依赖解析需要,构建自动生成)
<私有依赖>.dll *.xbf assets/…
demo.lwp ← 放在 plugins/ 根目录的插件包,启动时自动安装并删除
plugin.json
{
"id": "hw-monitor",
"name": "硬件监控",
"version": "1.0.0",
"entry_dll": "HardwareMonitor.dll",
"api_version": 2,
"min_host_version": "2.0.0",
"description": "可选描述",
"author": "可选作者",
"icon_glyph": "\uE950",
"homepage": "https://example.com",
"license": "MIT",
"tags": ["hardware"]
}
| 字段 | 必填 | 规则 |
|---|---|---|
id |
✔ | ^[a-z0-9][a-z0-9-]{1,63}$,全局唯一 |
name |
✔ | 显示名称 |
version |
✔ | 数字点分版本号(1.2.3) |
entry_dll |
✔ | 包内文件名,不能含路径;不能是 WinIsland.Core.dll |
api_version |
✔ | 当前为 2;必须 ≤ 宿主支持的版本 |
min_host_version |
✖ | 低于此宿主版本时拒绝加载 |
| 其余 | ✖ | 展示用 |
校验失败时插件会以 错误 状态出现在「插件管理」里,并写明具体字段和原因;日志在
%LocalAppData%\WinIsland\logs\plugin.<id>.log。
2. 最小插件
using WinIsland.Core;
namespace MyPlugin;
public sealed class MyPlugin : IslandPluginBase
{
protected override Task OnInitializeAsync()
{
Log.Info("插件已启动");
Context.Island.ShowMessage(new IslandMessage { Title = "你好", Text = "来自我的插件" });
return Task.CompletedTask;
}
protected override Task OnShutdownAsync()
{
Log.Info("插件已停用");
return Task.CompletedTask;
}
}
要求:public sealed class、实现 IIslandPlugin(或继承 IslandPluginBase)、有公共无参构造函数。
一个插件包只允许一个 IIslandPlugin 实现,多于一个或没有都会以明确错误终止加载。
3. 生命周期与状态
Discovered → Loaded → Active 正常运行
↘ Disabled 被用户禁用(持久化为 plugin.<id>.disabled)
↘ Faulted 初始化失败 / 运行期反复出错
Dispose/卸载 → Unloading → 程序集请求卸载并验证回收
InitializeAsync在启用循环中可能被多次调用(禁用→启用、重新加载),必须可重复执行。ShutdownAsync在停用前调用;即使它抛异常或写得不干净,宿主也会兜底撤销: 移除该插件注册的设置页、清空常驻内容、关闭它创建的定时器、退订设置变更、收回临时消息。- 非
Active/Loaded状态下调用 Island API 会被忽略并记日志(不会产生"停用后的幽灵行为")。 - 初始化超过 10 秒会被判定失败;单会话内 5 次未处理异常会自动停用插件并记为错误。
4. API 速查
插件通过 IPluginContext(IslandPluginBase 里是 Context,并提供了 Log/Settings/SetContent/RunOnUI 快捷方式)与宿主交互:
| 成员 | 说明 |
|---|---|
Manifest |
当前插件清单(Id/Name/Version/…) |
PluginDirectory |
插件自己的目录(读资源文件用) |
HostVersion / Dispatcher |
宿主版本 / UI 线程调度器 |
Log |
Debug/Info/Warn/Error,写入插件日志(内存环形缓冲 + 文件) |
Settings |
作用域化设置存储,键自动加 <id>. 前缀(Settings.Set("enabled", true) → hw-monitor.enabled) |
Island.SetContent(content) |
注册常驻内容(null 取消)。owner 由宿主绑定为插件 Id |
Island.OpenSpotlight(spotlight) / Island.CloseSpotlight() |
打开 / 收起「超级展开」聚光卡(见下方同名小节) |
Island.ShowMessage(msg) |
临时消息(标题 + 正文 + 图标 + 时长)。宽度跟随内容(152..340)、有正文时是 46 高的小卡;不给 AccentColor 时用与空闲点同色的中性图标芯片,而不是默认蓝色 |
Island.Show(uiElement, size, duration) |
临时展示任意控件 |
Island.AddSettingsPage(desc) |
注册设置页(停用时自动移除) |
Island.AddDropTarget(target) |
注册文件投放目标:拖文件到岛上时的一排卡片(见下方「文件投放」小节;停用时自动移除) |
Theme |
岛体当前的明暗主题(IIslandTheme:IsLight + Changed),配色适配用(见下方「主题与配色」;需宿主 ≥ 2.3.0) |
Register(IDisposable) |
登记需要在停用时释放的资源(订阅、原生句柄…) |
OnSettingsChanged(key, handler) |
监听某个设置键(handler 在 UI 线程调用) |
CreateTimer(interval, repeat, tick) |
UI 线程定时器(停用时自动停止并解绑) |
RunOnUI / RunOnUIAsync |
把工作编组回 UI 线程 |
常驻内容
// 1) 单视图变形(推荐):一个视图同时承载紧凑/展开形态
Context.Island.SetContent(new IslandLiveContent
{
Priority = 60,
MorphView = new MyMorphView(), // 实现 IMorphView:View + AnimateToExpanded/Compact
CompactSize = new Size(250, 40),
ExpandedSize = new Size(420, 150),
});
// 2) 双视图:紧凑与展开是两个独立控件
Context.Island.SetContent(new IslandLiveContent
{
CompactContent = compactPanel,
ExpandedContent = expandedPanel,
CompactSize = new Size(250, 40),
ExpandedSize = new Size(420, 150),
});
Priority 决定多个插件同时注册内容时谁占据主岛(数值大者优先,其余进入展开后的队列)。
小岛只显示主内容;展开后主内容 + 最多 3 个队列内容按大岛样式排列。
宿主可以在「设置 → 插件管理」里用 plugin.<pluginId>.priority 覆盖你声明的 Priority,所以不要依赖固定顺序。
尺寸由宿主统一:展开态所有元素同宽(取所有活动里最大的展开宽度)、所有队列卡片同高(取最高的一张),
宿主会把卡片拉伸到这个统一尺寸。视图请用自适应布局(Grid 的 * 行列、HorizontalAlignment/VerticalAlignment=Stretch),
不要假设自己一定能拿到 ExpandedSize 声明的精确尺寸。
形态动画不要用 Storyboard,必须逐帧直接赋值(适用于所有插件视图,包括纯代码自绘的可视树):
// 对:DispatcherQueueTimer 每帧直接给属性赋值
_morphTimer = DispatcherQueue.CreateTimer();
_morphTimer.Interval = TimeSpan.FromMilliseconds(16);
_morphTimer.IsRepeating = true;
_morphTimer.Tick += (_, _) => OnMorphTick();
void OnMorphTick()
{
var t = Math.Clamp((DateTimeOffset.UtcNow - _start).TotalMilliseconds / _duration.TotalMilliseconds, 0, 1);
_detail.Height = Math.Max(0, ExpandedDetailHeight * Ease(t)); // Height 会被缓动曲线推成负数,要夹住
_icon.Width = _icon.Height = CompactIcon + (ExpandedIcon - CompactIcon) * Ease(t);
}
原因是属性路径动画(Storyboard.SetTargetProperty(anim, "Height"))在动态加载的插件程序集里解析不出类型信息,
会在动画 tick 上抛 COMException (0x800F1001): Invalid attribute value Unknown for property Height。
这个异常发生在 tick 里,调用点的 try/catch 拦不住,会冒到宿主的未处理异常处理器:岛体尺寸已经收回去了、
插件的 Height 却卡在中间值,整块内容错位并且不再响应 hover(表现为"大岛变小之后卡死")。
宿主内置视图(Media/Battery)能安全使用 Storyboard,是因为它们在宿主程序集里,类型元数据可解析——插件侧不具备这个条件。
IMorphView 的调用点也要注意:视图可能被宿主放进展开队列的卡片里(QueuePanel),
契约要求 AnimateToExpanded/AnimateToCompact 能在任意时刻、任意父级下被调用;
动画进度要在视图里自己累计(如 _progress),这样"展开到一半又收起"才能从当前位置接着走,而不是跳回起点。
需要「临时不占岛」时,推荐像 samples/HardwareMonitor 那样加一个 enabled 设置:
关掉时 SetContent(null),打开时重新 SetContent(content),插件本身继续运行。
超级展开(Spotlight 聚光卡)
要展示的信息比大岛能装下的更多时,不要在岛里继续塞元素 —— 让点击打开「超级展开」:
一张居中的大卡片从岛体当前位置带倾角飞入并放大,点击卡片外区域或按 Esc 反向动画收回。
何时打开完全由插件决定(岛体 OnTap、定时器、外部事件都行),卡片尺寸与内容也由插件决定。
private void OpenDetail()
{
_detailView ??= new MyDetailView(_vm); // 必须是独立可视树,见下
Context.Island.OpenSpotlight(new IslandSpotlight
{
Content = _detailView,
Size = new Windows.Foundation.Size(760, 470), // 期望尺寸(DIP)
OnClosed = () => _detailView.OnHostClosed(), // 任何关闭路径都会回调一次
});
}
// 想程序化收起:Context.Island.CloseSpotlight();
| 成员 | 说明 |
|---|---|
Content |
卡片内容。必须是独立于岛视图的可视树:每个窗口一棵树,同一个 UIElement 不能同时挂在两个窗口里 |
Size |
期望尺寸(DIP)。宿主居中摆放并把尺寸夹到显示器工作区 92% 以内,视图请用自适应布局 |
OnClosed |
关闭回调(点卡片外 / Esc / 自己调 CloseSpotlight / 插件被停用 / 被别的插件替换)。宿主已做异常保护 |
必须知道的行为:
- 动画、遮罩、层级全由宿主负责,插件只提供一棵内容树;打开期间全屏点击都被覆盖窗接管 —— 这正是「点卡片外收起」的实现方式。
- 点插件自己的按钮/滑块不会触发
OnTap:宿主会顺着可视树上溯识别交互控件(ButtonBase/Slider/ToggleSwitch/ 自绘进度条等,自绘控件记得在Tapped里e.Handled = true),只有点内容空白处才算「点了岛体/卡片」。 - 生命周期跟着
Loaded/Unloaded走:收起时宿主会把内容从可视树卸下,视图收到Unloaded(定时器在Loaded里起、Unloaded里停);OnClosed用来做取消网络请求之类的收尾。视图实例可以复用,重复打开不必重建。 - 同一时刻只有一张聚光卡:别的插件再次请求时替换,旧 owner 会先收到
OnClosed;插件停用/卸载时宿主自动收起它的卡片,不留幽灵窗口。 - 宿主低于 2.1.0 时这些 API 不存在,调用会抛
MissingMethodException(宿主按插件异常捕获并记日志)。要用它就把plugin.json的min_host_version提到"2.1.0";api_version仍然是2(纯增量 API)。
文件投放(拖文件到岛上)
把文件/文件夹、文本、图片拖到岛上时,岛会展开成一排投放卡片,每张卡片是一个"松手就执行"的动作。 用户拖着内容悬停时卡片会放大高亮、指针压到两端时列表自动横向滚动、系统拖拽气泡显示「投放到「XXX」」, 左侧摘要会按载荷换样子(文件名 / 文本前两行 / 图片缩略图)。 展示、命中、滚动、动画全部由宿主接管,插件只负责「拿到内容之后干什么」。
Context.Island.AddDropTarget(new IslandDropTarget
{
Id = "add-to-playlist", // 插件内唯一;重复注册同一个 Id 是覆盖语义
Title = "加入播放列表",
Glyph = "\uE8C8", // Segoe Fluent Icons / Segoe MDL2 Assets 字形
Hint = "加进当前列表", // 可选副标题
AccentColor = Windows.UI.Color.FromArgb(255, 0x4C, 0xC2, 0xFF), // 可选,卡片高亮用它着色
Order = 100, // 升序;宿主内置的动作用 900+,插件默认 0 排在前面
Kinds = IslandDropKind.Files, // 接受哪些载荷,默认 Files
Extensions = new[] { ".mp3", ".flac" }, // 可选:文件的扩展名白名单(不填 = 全收)
Handler = async context =>
{
foreach (var path in context.Paths) await AddAsync(path);
return $"已加入 {context.Paths.Count} 首"; // 返回文案 → 宿主弹一条临时消息
},
});
// 三种载荷都能收(比如"存进历史"这种卡片)
Context.Island.AddDropTarget(new IslandDropTarget
{
Id = "remember", Title = "记入历史",
Kinds = IslandDropKind.All,
Handler = async context =>
{
switch (context.Kind)
{
case IslandDropKind.Text: await SaveTextAsync(context.Text); break;
case IslandDropKind.Image: await SaveImageAsync(context.ImageBytes); break;
default: await SavePathsAsync(context.Paths); break;
}
return "已记入历史";
},
});
| 成员 | 说明 |
|---|---|
Id / Title |
卡片标识与标题(必填) |
Glyph / Hint / AccentColor |
卡片外观:图标 / 副标题 / 高亮色,都可省。Hint 会显示在系统拖拽气泡里(卡片只有 72px 宽,副标题放不下)—— 卡片做什么一句话说不清时,这是唯一的说明位置 |
Order |
卡片顺序。插件目标默认排在宿主内置动作(打开 / 所在位置 / 复制路径 / 复制文本 / 保存图片)前面 |
Kinds |
接受哪些载荷:IslandDropKind.Files / Text / Image,可用 \| 组合,All 是全收。不匹配的载荷拖进来时这张卡片根本不出现(不是变暗) |
Extensions |
文件的扩展名白名单(含点、忽略大小写),只对 Files 生效。载荷里没有任何一项匹配时这张卡片同样不出现 |
Handler |
IslandDropContext → Task<string?>:在 UI 线程被调用,返回的文案由宿主弹出(返回 null 表示不提示) |
IslandDropContext 一次只带一种载荷:
| 成员 | 说明 |
|---|---|
Kind |
本次载荷类型(与卡片 Kinds 匹配的那一种) |
Paths / Names |
文件/文件夹的完整路径与文件名(Kind == Files 时有值) |
Text |
文本内容(Kind == Text 时有值;网址链接也走这里) |
ImageBytes |
图片原始字节,保留源格式(Kind == Image 时有值,通常是 PNG / JPEG) |
ItemCount / IsSingle |
这一批有多少项:文件是路径条数,文本/图片算 1 |
必须知道的行为:
- 只对文件、文本、图片触发:其它载荷(HTML 片段、自定义格式等)不会打开投放面板。 从浏览器拖图片时往往同时带文本(图片地址),宿主按 文件 > 图片 > 文本 的顺序判定。
- 不匹配的卡片不会出现:面板只展示"能对这份载荷做什么",一张都匹配不上时摘要会写明「没有卡片能接收它」。 但用户拖得很快(载荷还没读完就松手)时卡片会先全亮 —— 宿主会在松手瞬间按真实载荷复核一次,不匹配就当落空,不会误触发。
- 松手落空 = 什么都不做(绝不误触发动作),岛随即收回;拖出岛外或按
Esc取消同理。 - 图片有大小上限(32 MB):超过或读不出来时这张卡片等于落空,日志里会写明原因。
Handler抛异常不影响宿主:宿主把回调包在守卫里,只记日志并计入「累计 5 次未处理异常自动停用」。- 插件停用/卸载时卡片自动消失(
PluginScope兜底),注册过的投放目标不需要自己清理。 - 面板在松手时就已经收回,所以动作慢一点没关系:用户看到的是你返回的那条消息。
- 需要宿主 2.2.0 及以上:把
plugin.json的min_host_version提到"2.2.0"(api_version仍是2,纯增量 API)。 参考实现见宿主内置的五个动作:WinIsland/Core/DropTargets/HostDropTargets.cs。
主题与配色(浅色 / 深色适配)
岛的 Fluent 外观跟随系统明暗(设置 → 个性化 → 颜色 → 「默认应用模式」,进程存活期间切换也当场生效),Apple 外观恒为深色黑胶囊。插件视图必须两套都能看:写死 Colors.White → 浅色主题下白字压白底;浅色时建好、之后不重刷 → 系统切深色后变成深色岛上的黑字。
XAML 视图:文字用系统画刷,岛体换主题时它们自己跟着换;自定义中性色写进 ThemeDictionaries 的 Light / Dark 两套,再用 {ThemeResource 你的键} 取。
<TextBlock Text="标题" Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
<TextBlock Text="说明" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
代码搭的视图:构造时接收 IIslandTheme(Context.Theme / IslandPluginBase.Theme),按 theme.IsLight 选色;中性色做成共享 SolidColorBrush 字段,订阅 theme.Changed 时只改它们的 Color(一支画刷被多处引用,改一次全跟着变)。
public MyPluginView(PluginManifest manifest, IIslandTheme theme)
{
_theme = theme;
ApplyThemeColors();
_theme.Changed += ApplyThemeColors; // 岛体换主题(UI 线程)
...
_title.Foreground = _textBrush; // 用共享画刷,别 new 一支写死的
}
private void ApplyThemeColors()
{
_textBrush.Color = Neutral(255);
_faintBrush.Color = Neutral(160);
}
/// <summary>中性色:岛体深色时白色系,浅色(Fluent + 浅色系统)时黑色系。</summary>
private Windows.UI.Color Neutral(byte alpha) => _theme.IsLight
? Windows.UI.Color.FromArgb(alpha, 0, 0, 0)
: Windows.UI.Color.FromArgb(alpha, 255, 255, 255);
| 成员 | 说明 |
|---|---|
IIslandTheme.IsLight |
岛体当前是否浅色(Apple 风格恒为 false);这是岛体的明暗,不是系统主题,别自己去读注册表或用应用级主题代替 |
IIslandTheme.Changed |
主题变化(UI 线程触发):在这里重刷配色 |
- 聚光卡(见上)是另一棵可视树,同样要接
IIslandTheme:卡片本身也跟着岛体一起明暗切换。 - 需要宿主 2.3.0 及以上:把
plugin.json的min_host_version提到"2.3.0"(api_version仍是2,纯增量 API)。 - 自查:把系统主题切一遍(浅色↔深色),岛上的文字、灰底、分隔线都得跟着变。
5. 线程模型
InitializeAsync/ShutdownAsync/ 设置页工厂 /OnSettingsChanged/CreateTimer回调都在 UI 线程 执行。- 采样、网络、文件等耗时工作放到后台线程,然后用
Context.RunOnUI(...)更新 UI。 CreateTimer必须在 UI 线程调用;插件里创建的UIElement必须在 UI 线程创建。
6. 依赖与资源(最容易踩的坑)
插件可以自带任意 NuGet 依赖和本机 DLL,宿主会用 AssemblyDependencyResolver + 插件的 deps.json 从插件目录解析:
<PropertyGroup>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.3.1" PrivateAssets="all" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2526" PrivateAssets="all" ExcludeAssets="runtime" />
</ItemGroup>
解析规则(宿主侧):
- 框架程序集(
System.*/Windows.*/WinRT.*)与WinIsland.Core→ 始终绑定宿主,防止类型身份分裂。 - 其它程序集:宿主目录里存在同名 DLL → 用宿主那份;否则用插件目录里的副本。
- 因此:WinAppSDK/WinUI 运行时不必随插件分发(浪费 40MB),自己的依赖必须真的复制到插件目录
(
samples/HardwareMonitor的 csproj 里有一份可直接抄的过滤写法)。
本机(native)依赖也放在插件目录里,LoadUnmanagedDll 会一并在该目录解析。
6.1 构建用的 SDK 不能比宿主新
Microsoft.Windows.SDK.NET、WinRT.Runtime 这类 .NET 投影程序集由宿主提供(插件包里不带、也别带,见上面规则 1),
而 .NET 不允许向下绑定强命名程序集。插件若用比宿主新的 .NET SDK 构建,引用的投影版本就会高于宿主,加载时报:
插件需要 Microsoft.Windows.SDK.NET 10.0.26100.86,宿主提供的是 10.0.26100.38:
插件构建用的 .NET / Windows SDK 比宿主新,请用与宿主相同或更旧的 SDK 重新构建插件
(发生在静态构造里时还会被包成 TypeInitializationException。)正式版宿主由 CI 用 .NET 10.0.x 构建,
所以插件也必须钉在 .NET 10 —— 在插件仓库根目录放一个 global.json:
{
"sdk": {
"version": "10.0.200",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}
不钉的话 dotnet build 会挑机器上最高的 SDK(装了 .NET 11 预览版就会挑它),于是出现"本地能跑、装到正式版宿主上就报错":
本地宿主和插件是同一个新 SDK 构建的,正式版宿主不是。
实在没法换 SDK 时,可以只钉投影版本:
<PropertyGroup>
<WindowsSdkPackageVersion>10.0.26100.57</WindowsSdkPackageVersion>
</PropertyGroup>
值取宿主发布版 WinIsland.deps.json 里 runtimepack.Microsoft.Windows.SDK.NET.Ref/ 后面的版本号(宿主换 SDK 就得跟着改),
所以能加 global.json 就别走这条路。
7. 开发循环
# 插件工程里加一个构建后拷贝目标(见样例 csproj)
dotnet build
# 然后在「设置 → 插件管理」点「重新加载」,或用开关禁用→启用
日志:%LocalAppData%\WinIsland\logs\plugin.<id>.log(界面里「查看日志」也能看到最近 500 行)。
8. XAML 视图(可选路径)
动态加载的程序集不在应用的 resources.pri 里,生成的 InitializeComponent 找不到自己的 XBF。
需要 XAML 时用 SDK 提供的方法:
public sealed partial class MyView : UserControl, IMorphView
{
public MyView() => PluginXaml.Load(this); // 代替 InitializeComponent()
}
约束:
- XAML 文件与类同名,所在文件夹与命名空间一致(
MyPlugin.Views.MyView↔Views/MyView.xaml)。 - XAML 里只用框架类型,不要引用插件自己的自定义控件。
- 图片等资源用
Context.PluginDirectory拼绝对路径加载,不要用ms-appx:。 - 视图根元素保持透明背景:岛体材质由宿主绘制(Apple 风格是纯黑胶囊,Windows Fluent 风格是 Desktop Acrylic / Mica
系统材质),插件自绘不透明底色会在切换风格时露出与材质不一致的色块。卡片、徽标用半透明白(如
#33FFFFFF)即可适配两种材质。 - 形态动画同样要遵守上面的「逐帧直接赋值」规则——这条与是否使用 XAML 无关,XBF 树只是更容易触发的场景。
9. 打包与安装
# 把构建输出 + plugin.json 打成 .lwp(zip)
pwsh tools/pack-plugin.ps1 -ProjectDir samples\HelloPlugin
- 安装:把
.lwp拖进plugins/(启动时自动安装),或「插件管理 → 安装插件包…」。 - 更新:同 Id 的包会停用旧版本 → 卸载程序集 → 替换目录 → 重新启用。
- 删除:「插件管理 → 删除」,会真正移除插件目录;被占用的文件会在下次启动时清理。
- 包内不需要(也不应该)包含
WinIsland.Core.dll与 WinAppSDK 运行时文件,校验会拒绝前者。
发布到社区插件市场
「设置 → 插件市场」读取社区仓库 luolangaga/WinLandPlugin 根目录的 index.json:整个列表只发一次请求
(图标以 base64 内嵌在清单里,不做逐插件请求),只有点「安装」才下载 .lwp 并强制校验 SHA-256。
- 投稿:把
plugin.json+<id>.lwp+logo.png(可选)+README.md(可选)放进plugins/<id>/后提 PR, 合并后 Action 自动重建清单;仓库里的tools/submit-plugin.ps1负责本地打包与校验。 - 详情页的 README 由客户端自带的轻量渲染器渲染(
Settings/MarkdownRenderer.cs):支持标题、列表、表格、 围栏代码块、引用、行内样式与链接;图片不渲染(相对路径无从解析,退化为 alt 文本)。 plugin.json的id必须等于目录名,version/entry_dll必须与.lwp包内那份完全一致(CI 会校验)。- 市场源(可用
marketplace.baseUrl覆盖,留空即自动):官方raw.githubusercontent.com→ GitCode 国内镜像api.gitcode.com/api/v5/repos/luolangaga/WinLandPlugin/raw→ jsDelivr。 GitHub 是唯一源头,其余都只是镜像;客户端按顺序尝试并记住上次成功的源。
10. 样例
| 样例 | 内容 |
|---|---|
samples/HelloPlugin |
代码构建 UI、私有依赖、设置页、定时器、完整清理、受管异常演示 |
samples/XamlPlugin |
XAML 视图 + PluginXaml.Load + 逐帧形变动画 |
samples/HardwareMonitor |
真实功能插件:CPU/GPU/网络/帧率,自带 NuGet 依赖,设备选择,管理员提示 |
11. 从 1.x 迁移
| 1.x | 2.x |
|---|---|
IIslandModule(含 Id/DisplayName) |
IIslandPlugin + plugin.json 提供元数据 |
[IslandPlugin(...)] |
删除,改用 plugin.json |
InitializeAsync(IDynamicIslandApi) |
InitializeAsync(IPluginContext)(或重写 OnInitializeAsync) |
Api.SetLiveContent(Id, content) |
Context.Island.SetContent(content) |
Api.Settings(全局键) |
Context.Settings(自动加 <id>. 前缀,旧键名不变) |
Api.Settings.Changed += … |
Context.OnSettingsChanged(key, handler) 或 Register(...) 包装 |
Api.Dispatcher.CreateTimer() |
Context.CreateTimer(...)(自动随停用释放) |
根目录散装 *.dll |
plugins/<id>/ 目录或 .lwp 包 |
旧插件不迁移就不会被加载,并会在「插件管理」里提示检测到旧格式 DLL。
12. 故障排查
| 现象/日志 | 原因 |
|---|---|
| 程序集里没有找到 IIslandPlugin 实现 | 入口类不是 public sealed、抽象、或缺无参构造 |
| 缺少依赖程序集:xxx | 依赖没复制到插件目录(见 §6)或 deps.json 缺失 |
| 插件需要 X a.b.c,宿主提供的是 X d.e.f | 插件构建用的 .NET / Windows SDK 比宿主新(§6.1),加 global.json 钉住 SDK 后重新构建 |
| 插件针对 WinIsland.Core x.y 构建,与宿主不兼容 | SDK 大版本不一致,用 SDK 2.x 重新编译 |
| 已忽略调用 xxx:插件当前状态为 已停用 | 停用后仍有回调(定时器/网络回调),属正常保护 |
| 形态动画执行失败 | 插件动画抛异常(多为 §8 的 Storyboard 问题),宿主已忽略并记日志 |
| 聚光卡打不开 / 一片空白 | Content 复用了岛视图里那个 UIElement(每个窗口一棵树,必须新建视图);或宿主版本低于 2.1.0(日志里会有 MissingMethodException,plugin.json 写 min_host_version: "2.1.0") |
| 聚光卡一直不消失 | 卡片是模态的:点卡片外区域或按 Esc 收起,插件也可以自己调 CloseSpotlight() |
帧率显示 -- |
读取前台窗口帧率需要以管理员身份运行 WinIsland(ETW);普通权限下显示桌面合成帧率 |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0-windows10.0.26100 is compatible. |
-
net10.0-windows10.0.26100
- Microsoft.Windows.SDK.BuildTools (>= 10.0.28000.2526)
- Microsoft.WindowsAppSDK (>= 2.3.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.