首页
/ PowerToys Run(PowerLauncher)插件体系解析:IPlugin 接口、生命周期与插件配置机制

PowerToys Run(PowerLauncher)插件体系解析:IPlugin 接口、生命周期与插件配置机制

2026-09-06 12:43:06作者:秋阔奎Evelyn

PowerToys Run(源码中称 PowerLauncher / PT Run)是 Microsoft PowerToys 中的启动器模块,其可扩展性的核心在于一套统一的插件契约:所有插件(计算器、文件索引器、窗口切换、Web 搜索等)都实现同一接口,由宿主 PluginManager 统一加载、初始化、分发查询与同步设置。本文基于仓库中的开发文档与 src/modules/launcher 下的真实源码,完整梳理每个插件共同遵循的生命周期函数(InitQueryUpdateSettingsThemeChangedSave)、上下文菜单图标、结果打分(Score)机制,以及 plugin.jsonsettings.json 两级配置的落地方式,帮助开发者理解并编写符合 PowerToys Run 规范的插件。

PowerToys Run 视图模型与插件交互示意图

IPlugin:每个插件必须实现的统一契约

文档 doc/devdocs/modules/launcher/plugins/overview.md 指出:每个插件都实现 IPlugin 接口,该接口由 Init()Query() 两个核心函数构成。在当前仓库中,接口定义位于 IPlugin.cs,其完整形态如下:

namespace Wox.Plugin
{
    public interface IPlugin
    {
        List<Result> Query(Query query);

        void Init(PluginInitContext context);

        // Localized name
        string Name { get; }

        // Localized description
        string Description { get; }
    }
}

从源码结构看,接口比文档描述还多了两个属性:

  • Name / Description:本地化的插件名称与描述,供设置界面展示。源文件中保留了被注释掉的 public static abstract string PluginID 属性——注释说明其为 plugin.json 条目校验之用,且必须为静态以便在加载插件前访问,但当前因单元测试所依赖的 Moq 包尚不支持 .NET 7 的 static abstract 特性而被注释。
  • Query(query) 返回 List<Result>:插件根据用户查询词返回结果集合,这是插件对外提供价值的唯一出口。
  • Init(context):插件初始化入口,见下文。

以计算器插件为例,Calculator/Main.cs 中的入口类声明为 public class Main : IPlugin, IPluginI18n, IDisposable, ISettingProvider,体现了文档中“Init()Main.cs 中第一个被调用的函数”的约定:每个插件项目都有一个名为 Main.cs 的入口文件,宿主按 DLL 加载后调用其中的 Main 实例。此外,插件还会按需扩展其他可选契约:IPluginI18n(提供翻译后的标题/描述)、IDisposable(宿主侧的资源释放,如取消订阅主题事件)、ISettingProvider(实现 UpdateSettings,见下文)。

Init:插件的“构造函数”

Init() 负责初始化插件的上下文、存储与设置,等价于构造函数。它的签名接收一个 PluginInitContext,该类的定义在 PluginInitContext.cs

public class PluginInitContext
{
    public PluginMetadata CurrentPluginMetadata { get; internal set; }

    /// <summary>
    /// Gets or sets public APIs for plugin invocation
    /// </summary>
    public IPublicAPI API { get; set; }
}

也就是说,初始化时插件拿到两样东西:

  1. CurrentPluginMetadata:该插件 plugin.json 解析出的元数据;
  2. APIIPublicAPI):宿主暴露给插件的公共 API 门面,插件通过它调用查询改写、主题订阅等能力。

计算器插件的 Init 实现是教科书式的示范(见 Calculator/Main.cs):

public void Init(PluginInitContext context)
{
    Context = context ?? throw new ArgumentNullException(paramName: nameof(context));
    Context.API.ThemeChanged += OnThemeChanged;
    UpdateIconPath(Context.API.GetCurrentTheme());
}

它在初始化时做了两件事:订阅宿主的 ThemeChanged 事件,并立即根据当前主题设置图标路径。对应的 Dispose 实现中会执行 Context.API.ThemeChanged -= OnThemeChanged 取消订阅——这解释了为什么插件入口类要实现 IDisposable:防止宿主重复加载/卸载插件时事件委托泄漏。

Query:每次用户输入都触发的查询执行

对于用户在 PT Run 中键入的每一次查询,宿主都会执行每个(被路由命中的)插件 Main.cs 中的 Query() 函数。查询的载体是 Query 对象,其中携带 Search(去掉动作关键词后的实际查询词)、RawQuery(原始输入)与 ActionKeyword(命中的动作关键词,若为空则表示这是一次“全局查询”——即用户未输入任何前缀关键词)。

计算器插件的 Query 实现(Calculator/Main.cs)展示了几个典型的插件编写模式:

public List<Result> Query(Query query)
{
    ArgumentNullException.ThrowIfNull(query);

    bool isGlobalQuery = string.IsNullOrEmpty(query.ActionKeyword);
    bool replaceInput = _replaceInput && !isGlobalQuery && query.Search.EndsWith('=');
    ...
    // Happens if the user has only typed the action key so far
    if (string.IsNullOrEmpty(query.Search))
    {
        return new List<Result>();
    }
    ...
}
  • 空查询快速返回:用户仅输入了动作关键词时直接返回空列表;
  • 通过 API 改写用户输入:当启用了“替换输入”选项且输入以 = 结尾时,插件调用 Context.API.ChangeQuery($"{query.ActionKeyword} {pluginResult.QueryTextDisplay}") 把输入替换为计算结果,实现“输入 =2+3 得到 =5”的交互;
  • 异常兜底:捕获 ParseExceptionOverflowException 与通用 Exception,通过 ErrorHandler.OnError 将错误作为结果返回,确保任何插件崩溃都不会拖垮整个宿主进程。

Score:结果排序依据相关性打分

文档明确说明:用户查询会针对每个插件执行,结果列表视图由所有插件的结果共同填充,而结果的排列顺序基于每个 ResultScore。每个插件根据自身判断的相关性给结果赋分——分数越高,在列表视图中位置越靠前,反之越靠后。换言之,宿主并不硬编码任何模块间的优先级,跨插件的排序完全由各插件自报的分数驱动;插件开发者应保证分数与“结果对当前查询的匹配程度”单调一致,这是结果列表可读性的关键。

上下文菜单图标

每条结果还可以附带上下文菜单(ContextMenus),按结果类型加载。文档列举了仓库中常见的上下文菜单功能类型:

  • Open containing folder(打开所在文件夹)
  • Run as Administrator(以管理员身份运行)
  • Open in console(在控制台打开)
  • Copy path(复制路径)

这类菜单项在文件索引器、程序搜索等插件中最为典型,例如程序搜索插件会为命中的可执行文件挂载“打开文件位置 / 以管理员身份运行”等菜单项,让用户无需先打开程序即可完成二级操作。

UpdateSettings:设置 UI 变更的落地点

UpdateSettings 负责把用户在 PowerToys 设置界面中所做的更改同步进插件运行时。文档给出的例子是:在文件索引器插件中禁用磁盘检测——当用户勾选或取消“驱动检测”复选框时,UpdateSettings() 会把复选框的变更分发到插件实例。

从源码看,宿主在设置变更时调用插件入口类上的 UpdateSettings(PowerLauncherPluginSettings settings) 方法(需实现 ISettingProvider 契约)。计算器插件的实现(Calculator/Main.cs)展示了标准做法:

  1. 先为本插件支持的每个选项声明带默认值的局部变量(如 replaceInput = truetrigMode = Radians);
  2. settings.AdditionalOptions 中按 Key 逐一查找,存在则以设置值覆盖默认值;
  3. 对可能解析失败的选项(如下拉框的整型值)单独 try/catch,失败时记录日志并保留默认值,保证单个选项损坏不影响其他选项;
  4. 最后将解析结果写入插件私有字段(_inputUseEnglishFormat 等),供后续 Query() 使用。

插件声明自己支持哪些设置项的方式是实现 AdditionalOptions 属性:计算器在 Calculator/Main.cs 中声明了“输入/输出使用英文格式”“替换输入”以及“三角函数单位(弧度/角度/梯,Combobox 类型)”四个选项,设置 UI 会自动据此渲染复选框与下拉框,用户改动后触发上面的 UpdateSettings 流程——这与文档中“设置从 UI 变更分发到插件”的描述完全对应。

ThemeChanged 与 IconPath:主题切换时的图标更新

当 PT Run 的主题发生变化时,宿主触发主题变更事件,插件据此更新自身的 IconPath。计算器插件的实现非常直观:

private void UpdateIconPath(Theme theme)
{
    if (theme == Theme.Light || theme == Theme.HighContrastWhite)
    {
        IconPath = "Images/calculator.light.png";
    }
    else
    {
        IconPath = "Images/calculator.dark.png";
    }
}

注意这里的双通道设计:运行时主题切换走 ThemeChanged 事件回调;而 plugin.json 中的 IcoPathDark / IcoPathLight 字段则服务于插件加载阶段(设置面板、插件列表等 UI 在调用 Init 前就需要展示图标),两者互补。

Save:持久化插件配置

Save 用于把插件当前的配置落盘,以便下次启动时恢复。宿主 PluginManager 中提供了静态的 Save() 入口(见 PluginManager.cs),在插件集合或相关状态变化时被调用,将全部插件的当前设置统一写出;这与下文“插件设置存储于 PowerToys Run\settings.json”的机制相衔接。

插件的宿主侧管理:PluginManager

文档中提到的“PluginManager.cs 执行每个插件的 Query()”对应源码 PluginManager.cs(位于 src/modules/launcher/PowerLauncher/Plugin/,共 338 行)。从源码结构看,它承担了插件体系的全部宿主侧职责:

  • 插件发现与去重AllPlugins 属性从 Constant.PreinstalledDirectory(预装目录)与 Constant.PluginsDirectory(用户插件目录)两处解析 plugin.json,只保留 Language 为 C# 的插件,并按插件 ID 分组——同一 ID 存在多份 DLL 时(如升级未清理旧版本),选取产品版本最高的一份;
  • 全局 / 非全局划分GlobalPlugins 返回 Metadata.IsGlobal == true 的插件(任何输入都会参与查询);NonGlobalPlugins 返回配置了非空 ActionKeyword 的插件(仅当输入以该前缀触发时才参与查询);
  • 测试支持:暴露 SetAllPlugins 静态方法,仅供测试注入替身插件列表(源码注释“should be only used in tests”);配套的单测位于 Wox.Test/PluginManagerTest.cs

插件设置:plugin.json 与 settings.json 两级结构

文档“Plugin settings”一节的关键结论有三点,均可在仓库中得到印证:

  1. 可编辑设置存储在 PowerToys Run\settings.json:即各插件在设置 UI 中被用户修改后的 AdditionalOptions 值最终落在这里,并在下次启动时由 UpdateSettings 读取;
  2. 首次运行时,设置从插件的 plugin.json 填充plugin.json 是插件的“出厂默认 + 元数据”清单,首次启动后宿主以其为种子生成 settings.json 中对应条目;
  3. 不支持多个动作关键词:与上游 Wox 不同,PowerToys Run 每个插件只有一个 ActionKeyword 与一个 IsGlobal 开关,没有多关键词列表。

以计算器插件的 plugin.json 为例:

{
  "ID": "CEA0FDFC6D3B4085823D60DC76F28855",
  "ActionKeyword": "=",
  "IsGlobal": true,
  "Name": "Calculator",
  "Author": "cxfksword",
  "Version": "1.0.0",
  "Language": "csharp",
  "Website": "https://aka.ms/PowerToys",
  "ExecuteFileName": "Microsoft.PowerToys.Run.Plugin.Calculator.dll",
  "IcoPathDark": "Images\\calculator.dark.png",
  "IcoPathLight": "Images\\calculator.light.png"
}

各字段的作用:ID 为插件唯一标识(也是源码中 Main.PluginID 常量与之保持一致、用于校验 plugin.json 的依据);ActionKeyword: "=" 表示用户以 = 开头时触发该插件;IsGlobal: true 表示它同时参与全局查询——这正是计算器既能写 =2+3 又能直接写 2+3 的原因;ExecuteFileName 指明宿主要加载的 DLL;IcoPathDark / IcoPathLight 指明两种主题下的插件图标。

仓库内置插件一览

按上述契约,仓库中预装了约二十个 C# 插件,均位于 src/modules/launcher/Plugins/,与文档 doc/devdocs/modules/launcher/plugins/ 下的逐插件说明文档一一对应:

插件项目 对应文档
Microsoft.PowerToys.Run.Plugin.Calculator calculator.md
Microsoft.Plugin.Indexer indexer.md
Microsoft.Plugin.Program program.md
Microsoft.Plugin.Folder folder.md
Microsoft.Plugin.WindowWalker windowwalker.md
Microsoft.PowerToys.Run.Plugin.WebSearch websearch.md
Microsoft.PowerToys.Run.Plugin.TimeDate timedate.md
Microsoft.PowerToys.Run.Plugin.WindowsSettings windowssettings.md
Microsoft.PowerToys.Run.Plugin.Registry registry.md
Microsoft.PowerToys.Run.Plugin.System system.md
Community.PowerToys.Run.Plugin.UnitConverter community.unitconverter.md
Community.PowerToys.Run.Plugin.ValueGenerator community.valuegenerator.md
Microsoft.Plugin.Shell / Microsoft.Plugin.Uri / Microsoft.PowerToys.Run.Plugin.History shell.md / uri.md / history.md

如需扩展插件体系,仓库还提供了 new-plugin-checklist.mdarchitecture.mddebugging.md 三份配套文档,分别覆盖新插件开发清单、整体架构与调试方法。

小结

PowerToys Run 的插件模型可以浓缩为一条清晰的生命周期链:PluginManager 扫描两级插件目录并解析 plugin.json(按 ID 去重、按版本择优)→ 为插件构造 PluginInitContext 并调用 Init()(插件在此订阅主题事件、读取存储与设置)→ 用户每次输入时按 ActionKeyword / IsGlobal 路由并调用 Query()(插件返回带 ScoreResult 列表,宿主据此排序渲染)→ 设置界面变更触发 UpdateSettings(),主题切换触发 ThemeChanged,配置通过 Save() 写入 PowerToys Run\settings.json 持久化。理解这条链路,并参照计算器插件中 Init / Query / UpdateSettings / 主题回调的完整实现,就具备了为 PowerToys Run 编写行为正确、设置可持久、主题可适配的插件的全部基础。

登录后查看全文
热门项目推荐
相关项目推荐