首页
/ PowerToys 新模块开发端到端指南:从模块模板到设置集成、调试与打包

PowerToys 新模块开发端到端指南:从模块模板到设置集成、调试与打包

2026-09-04 22:04:49作者:宣聪麟

本文基于 PowerToys 官方开发者文档 Creating a new PowerToy: end-to-end developer guide 整理扩写,完整覆盖从零构建一个 PowerToys 工具模块的全流程:模块类型选型、模块接口(Module Interface)的关键方法、服务工程搭建、设置系统集成、调试技巧、WiX 安装包集成以及测试与 OOBE 收尾。读完后,你可以独立完成一个新模块从模板初始化、runner 注册、设置页面接线到可打包交付的全部工作,并理解 runner 与模块 DLL 之间的加载机制。

1. 概览与前置条件

PowerToy 模块是一个自包含(self-contained)的工具单元,集成在 PowerToys 生态内,可以是纯 UI 型、纯后台服务型,或者两者兼有。

1.1 环境要求

  • 先按照 Getting Started 指南配置好开发环境,然后参照 调试文档 验证自己能够构建并运行 PowerToys.slnx
  • 可选:WiX v5 工具集(用于制作安装包)。

1.2 标准目录结构

原文档约定的模块目录布局如下,所有模块逻辑都应隔离在 src/modules/<YourModule> 之下:

src/
  modules/
    your_module/
      YourModule.sln
      YourModuleInterface/
      YourModuleUI/ (if needed)
      YourModuleService/ (if needed)

从源码结构看,ModuleInterface 工程产出的是 ModuleInterface.dll,供 runner 在运行时加载并调用;UI 与 Service 工程则按需拆分。

2. 设计与规划:先确定模块类型,再写接口

2.1 选择模块类型

写代码前先想清楚三件事:需要什么样的 UI、生命周期如何、它是常驻服务还是事件驱动。文档给出了四类典型场景,并推荐对照相似的现有模块来研究:

模块主体既可以用 C++ 编写,也可以用 C# 编写。

2.2 模块接口的关键成员

模块入口是 ModuleInterface(模板中为 dllmain.cpp),核心类继承自 PowertoyModuleIface。当前仓库中的接口定义位于 powertoy_module_interface.h,其中声明了 get_nameget_keyget_configset_configenabledisableis_enableddestroy 等纯虚函数,以及带默认实现的 get_hotkeyson_hotkeyis_enabled_by_defaultgpo_policy_enabled_configuration 等方法(见该文件约 89–156 行)。文档要求你在模板中理解并改造以下九组关键成员:

(1)设置结构体 ModuleSettings

这是模块设置项的存放处,值类型可以是字符串、bool、int,甚至自定义枚举:

struct ModuleSettings {};

(2)接口类声明

完整类定义继承 PowertoyModuleIface,私有成员通常包含启用状态、事件处理逻辑或热键相关字段,公有部分包含构造函数与初始化逻辑:

class ModuleInterface : public PowertoyModuleIface
{
  private:
    // the private members of the class
    // Can include the enabled variable, logic for event handlers, or hotkeys.
  public:
    // the public members of the class
    // Will include the constructor and initialization logic.
}

注意:类中许多函数是样板代码,只需把模块名做简单的字符串替换;下面列出的其余函数才需要较大改动。

(3)GPO 组策略支持

GPO(Group Policy Object)允许管理员在一组机器上统一下发策略。你的模块必须出现在 GPO 的设置列表中,并实现 gpo_policy_enabled_configuration 返回对应模块的策略值。实现上可以右键 powertoys_gpo 对象跳转到定义,为模块配置 getConfiguredModuleEnabledValue

virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
    return powertoys_gpo::getConfiguredModuleEnabledValue();
}

(4)init_settings():初始化设置

从已存在的 settings.json 读取配置,文件不存在时保留默认值:

void ModuleInterface::init_settings()

(5)get_config:向设置面板描述配置

Runner 调用它获取 settings.json 中该模块的配置描述(序列化为 JSON 写入缓冲):

virtual bool get_config(wchar_t* buffer, int* buffer_size) override

(6)set_config:接收新设置

设置面板提交的新值会以序列化 JSON 传入,模块负责解析并持久化:

virtual void set_config(const wchar_t* config) override

(7)call_custom_action:自定义动作

当模块使用 custom_action 类型的设置项时,设置面板点击按钮会触发该方法:

void call_custom_action(const wchar_t* action) override

(8)生命周期函数

控制模块的启用/禁用状态以及默认是否启用:

virtual void enable() // starts the module
virtual void disable() // terminates the module and performs any cleanup
virtual bool is_enabled() // returns if the module is currently enabled
virtual bool is_enabled_by_default() const override // allows the module to dictate whether it should be enabled by default in the PowerToys app.

(9)热键函数

负责热键的解析、上报与响应:

// takes the hotkey from settings into a format that the interface can understand
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)

// returns the hotkeys from settings
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override

// performs logic when the hotkey event is fired
virtual bool on_hotkey(size_t hotkeyId) override

2.3 设计原则

  • 模块逻辑隔离在 /modules/<YourModule> 下,禁止跨模块直接依赖;
  • 优先复用 src/common 中的共享工具库(如 DPI 辅助 dpi_aware.h、显示器枚举 monitors.h 等);
  • init/set/get config 都通过预设函数访问设置,核心实现在 src/common/SettingsAPIsettings_helpers.hsettings_objects.h 中,PowerToysSettings::Settings 支持 add_bool_toggleadd_int_spinneradd_stringadd_color_pickeradd_custom_action 等控件类型,PowerToyValues 提供 load_from_settings_file / save_to_settings_file 持久化方法。

3. 模块脚手架(Bootstrapping)

  1. 使用 PowerToy 模块模板 生成模块接口的起始代码。模板安装方式见 tools/project_template/README.md:把 ModuleTemplate.zip 放入 %USERPROFILE%\Documents\Visual Studio 2022\Templates\ProjectTemplates\(VS 2026 对应 Visual Studio 18 目录),之后在 Visual Studio 新建工程时于 Visual C++ 选项卡下即可看到。

  2. 把全部工程与命名空间替换为你的模块名。模板文件 dllmain.cpp 中使用 $projectname$ / $safeprojectname$ 占位符,例如 const static wchar_t* MODULE_NAME = L"$projectname$";

  3. 更新 .vcxproj 与解决方案文件中的 GUID。

  4. 用你自己的逻辑实现第 2 节提到的各函数。

  5. 注册模块——这是模块能被 runner 检测到的必要步骤。原文档列出的注册清单包括:

    • src/runner/modules.h
    • src/runner/modules.cpp
    • src/runner/resource.h
    • src/runner/settings_window.h
    • src/runner/settings_window.cpp
    • src/runner/main.cpp
    • src/common/logger.h(日志)

    小技巧:在 runner 代码中搜索已有模块名(如 LightSwitch)可以快速定位这些清单。从当前仓库源码可以确认:runner 在 src/runner/main.cpp 中维护了 knownModules 列表,逐项形如 L"PowerToys.LightSwitchModuleInterface.dll",随后在循环中调用 load_powertoy(moduleSubdir) 加载并以 pt_module->get_key() 为键存入 modules();设置页映射在 src/runner/settings_window.hESettingsWindowNames 枚举与 src/runner/settings_window.cpp 中成对出现;模板 README 还补充说明模块 DLL 名需加入 src/runner/main.cppknown_dlls 映射才能在运行时被加载。

  6. ModuleInterface 工程必须产出 ModuleInterface.dll,这样 runner 才能与服务交互。

经验提示:模块 ID 不一致(manifest、注册表、服务之间)是最常见的加载失败原因之一,务必保持一致。

4. 编写服务(Service)

每个 PowerToy 的服务形态都不同。建议先在独立工程中开发应用主体,再接入 PowerToys 的设置逻辑;但服务必须先于 runner 接线完成。

要点:

  • 服务是与 Module Interface 相互独立的项目,可用 C# 或 C++ 编写;
  • 服务图标通过 .rc 文件设置;
  • 服务名在 .vcxproj 中通过 <TargetName> 设置,例如:
<PropertyGroup>
  <OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
  <TargetName>PowerToys.LightSwitchService</TargetName>
</PropertyGroup>
  • 需要查看 .vcxproj 内容时,右键工程选择 Unload project
  • 服务内读取设置的推荐写法(ModuleSettings 单例随服务代码提供,可参考 src/modules/LightSwitch 中的实现并按需裁剪):
ModuleSettings::instance().InitFileWatcher();
ModuleSettings::instance().LoadSettings();
auto& settings = ModuleSettings::instance().settings();

如果模块带用户界面:

  • 使用 WinUI Blank App 模板建工程;
  • 遵循 Windows 设计最佳实践;
  • 借助 WinUI 3 Gallery 应用辅助 UI 编码。

5. 设置系统集成

PowerToys 的设置按模块以 JSON 形式存放在:

%LOCALAPPDATA%\Microsoft\PowerToys\<module>\settings.json

5.1 C# 侧实现步骤

  1. src\settings-ui\Settings.UI.Library\ 下创建 <module>Properties.cs<module>Settings.csProperties 中定义所有设置的默认值,需与 Module Interface 中声明的设置项一一对应;
  2. <module>Settings.cs 负责构建 settings.json 对象,结构应匹配:
public ModuleSettings()
{
    Name = ModuleName;
    Version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
    Properties = new ModuleProperties(); // settings properties you set above.
}
  1. src\settings-ui\Settings.UI\ViewModels 下创建 <module>ViewModel.cs——它是 PowerToys 应用内设置页与磁盘设置文件之间的交互层。此处的变更会通过 NotifyPropertyChanged 事件触发设置监听器;
  2. src\settings-ui\Settings.UI\SettingsXAML\Views 创建 SettingsPage.xaml,即用户与模块设置交互的页面;
  3. 面向用户的字符串必须走资源串以便本地化(x:Uid 关联 Resources.resw):
// LightSwitch.xaml
<ComboBoxItem
    x:Uid="LightSwitch_ModeOff"
    AutomationProperties.AutomationId="OffCBItem_LightSwitch"
    Tag="Off" />

// Resources.resw
<data name="LightSwitch_ModeOff.Content" xml:space="preserve">
  <value>Off</value>
</data>

重要:上面示例用 .Content 定位 ComboBox 的内容,这个后缀会随控件类型变化(例如 .Text.Header 等)。

提醒:通过外部编辑器(VS Code、记事本)的手工修改不会触发设置监听器,只有通过 PowerToys 写入的变更才会触发重载。这一行为与 C++ 侧的文件监听机制(src/common/SettingsAPI 中的 FileWatcher.h)相对应。

5.2 常见坑

  • 只使用 WinUI 3 框架,不要使用 UWP;
  • 从非 UI 线程更新 UI 时必须使用 DispatcherQueue

6. 构建与调试

6.1 调试步骤

  1. 首次调试 PowerToys 的开发者,请先完成 调试文档 中的预调试准备;
  2. runner 设为启动项目,并确认构建配置与系统架构(ARM64/x64)一致;
  3. F5 或点击 Local Windows Debugger 按钮开始调试,runner 会随之启动;
  4. 若要为服务设断点,按 Ctrl+Alt+P 搜索你的服务进程并附加到 runner;
  5. 用日志记录改动。日志位置:
    • Runner 日志:%LOCALAPPDATA%\Microsoft\PowerToys\RunnerLogs
    • 模块日志:%LOCALAPPDATA%\Microsoft\PowerToys\Module\Service\<version>

提示:PowerToys 会激进缓存 .nuget 产物,构建行为异常时可用 git clean -xfd 清理。

runner 侧的加载行为可以在 src/runner/main.cpp 中看到佐证:Debug 模式下某个模块加载失败只会 Logger::warn 记录并继续执行,便于开发者快速迭代,不必为调试单个模块而构建全部模块。

7. 安装包与打包(WiX)

7.1 把模块加入安装器

  1. 通过 NuGet 为 WiX5 安装 WixToolset.Heat
  2. installer\PowerToysInstallerVNext 目录为你的模块新增一个 Module.wxs 文件(例如 installer/LightSwitch.wxs 可作为参照格式);
  3. 拷贝其他模块(如 Light Switch)的 wxs 格式,替换字符串与 GUID 值;
  4. 关键占位符是 <!--ModuleNameFiles_Component_Def-->——它会被 generateFileComponents.ps1 生成的组件代码替换;当前仓库中对应的生成脚本为 installer/generateAllFileComponents.ps1
  5. installer/Product.wxs<Feature Id="CoreFeature" ... > 段落中加入一行 <ComponentGroupRef Id="ModuleComponentGroup" />
  6. 在文件组件生成脚本末尾按如下格式为新模块追加条目(-fileListName <Module>Files 需与 Module.wxs 中设置的字符串一致,<ModuleServiceName> 需与服务 exe 名一致):
# Module Name
Generate-FileList -fileDepsJson "" -fileListName <Module>Files -wxsFilePath $PSScriptRoot\<Module>.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\<ModuleServiceName>"
Generate-FileComponents -fileListName "<Module>Files" -wxsFilePath $PSScriptRoot\<Module>.wxs -regroot $registryroot

8. 测试与验证

8.1 UI 测试

  • 测试工程放在 /modules/<YourModule>/Tests
  • 新建 WinUI Unit Test App;
  • 参照现有模块(如 Light Switch)的测试写法,可测试独立的 UI(如 Color Picker 类模块),也可以验证 PowerToys 应用内的设置 UI 是否真正控制到了你的服务。

8.2 手动验证清单

  • 在 PowerToys Settings 中启用/禁用模块;
  • 检查日志中的初始化记录;
  • 确认图标、工具提示(tooltips)与 OOBE 页面正确显示。

8.3 实用技巧

  1. 验证睡眠/唤醒与提权(elevation)状态。后台模块若在事件句柄未在恢复后重建,唤醒后往往静默失效;
  2. 用 Windows Sandbox 模拟干净安装环境;
  3. 想模拟“新用户”,可删除 %LOCALAPPDATA%\Microsoft 下的 PowerToys 文件夹。

8.4 快捷键冲突检测

如果模块带快捷键,必须按设置实现文档中 Shortcut conflict detection 章节的步骤正确注册,以获得冲突检测能力。runner 侧的冲突检测实现可参考 src/runner/hotkey_conflict_detector.cpp 与集中式热键管理 src/runner/centralized_hotkeys.cpp

9. 收尾工作

9.1 OOBE(Out-of-Box Experience)页面

OOBE 页面是一个自定义设置页,在新用户首次使用以及更新之后、正式设置应用打开之前显示,让用户一眼了解各模块用途。需要:

  • src\settings-ui\Settings.UI\SettingsXAML\OOBE\Views 创建 OOBE<ModuleName>.xaml
  • 把模块名加入 src\settings-ui\Settings.UI\OOBE\Enums\PowerToysModules.cs 中的枚举。

9.2 模块资源(Assets)

模块功能完成后需要规划对外展示的资源:

  • Module Icon:显示在 OOBE 页面、README、PowerToys 主页、模块设置页等多处;
  • Module Image:各模块设置页顶部的图片;
  • OOBE Image:OOBE 页面上每个模块的头部图。

说明:图标与截图的具体设计由设计团队在应用内部保证一致性。如果你有关图标或截图的构想,可以写在 PR 的 "Additional Comments" 部分供团队参考。

9.3 文档

提交新 PowerToy 需要两类文档:

  1. 开发者文档:放在仓库 /doc/devdocs/modules/(如 doc/devdocs/modules/readme.md 目录),面向开发者,说明如何接手维护你的模块,应涵盖架构、关键文件、测试与调试技巧;
  2. Microsoft Learn 文档:当模块准备合入 PowerToys 仓库时,由内部团队成员编写面向用户的 Learn 文档。开发者在此步骤工作量不大,但需留意 PR 动态,及时补充团队索要的信息。

10. 小结:新模块接入检查单

阶段 关键动作 验证方式
设计 确定 UI-only / 服务 / 混合 / 互操作类型 找到最相似的现有模块作参照
脚手架 模板生成、替换占位符与 GUID ModuleTemplate.dll 工程可编译
注册 加入 runner 的模块 DLL 清单与设置窗口映射 src/runner/main.cppknownModulessettings_window.h 中搜到模块名
服务 独立工程先行,TargetName 命名,ModuleSettings 读设置 服务可独立运行并读写 %LOCALAPPDATA%\Microsoft\PowerToys\<module>\settings.json
设置集成 Properties/Settings/ViewModel/XAML/resw 五件套 设置面板改动触发文件监听并写回 JSON
调试 runner 为启动项目,Ctrl+Alt+P 附加服务 RunnerLogs 与模块 Module\Service 日志正常
打包 Module.wxs + Product.wxs 引用 + 生成脚本条目 安装包生成成功,干净环境(Windows Sandbox)验证
测试 WinUI 单元测试 + 手动验证 + OOBE 检查 启用/禁用、唤醒恢复、新用户场景均通过

如果你在使用过程中需要帮助,按文档建议:提一个带 Needs-Team-Response 标签的 issue 以获得团队关注。

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