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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="luolan.winland.Core" Version="2.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="luolan.winland.Core" Version="2.3.0" />
                    
Directory.Packages.props
<PackageReference Include="luolan.winland.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add luolan.winland.Core --version 2.3.0
                    
#r "nuget: luolan.winland.Core, 2.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package luolan.winland.Core@2.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=luolan.winland.Core&version=2.3.0
                    
Install as a Cake Addin
#tool nuget:?package=luolan.winland.Core&version=2.3.0
                    
Install as a Cake Tool

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>

解析规则(宿主侧):

  1. 框架程序集(System.* / Windows.* / WinRT.*)与 WinIsland.Core → 始终绑定宿主,防止类型身份分裂。
  2. 其它程序集:宿主目录里存在同名 DLL → 用宿主那份;否则用插件目录里的副本。
  3. 因此: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 Compatible and additional computed target framework versions.
.NET net10.0-windows10.0.26100 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.3.0 36 9/24/2026
2.2.1 57 9/24/2026
2.2.0 64 9/24/2026
1.1.0 115 7/28/2026
1.0.0 121 7/27/2026