PowerToys 核心架构解析:Runner、模块接口与设置系统的协作机制
本篇基于 PowerToys 官方开发文档 doc/devdocs/core/architecture.md,系统讲解 PowerToys 的三层核心架构——PowerToys Runner(主程序)、标准化的模块接口(Module Interface DLL)以及 Settings 设置系统,并结合 src/modules/interface/powertoy_module_interface.h、src/runner 等真实源码,说明每个设计决策背后的实现依据。读完本文,你将能够回答:一个新的 PowerToy 模块如何被 Runner 加载、启用、接收热键和配置变更,以及模块与设置 UI 之间如何跨进程通信。
一、架构总览:一个 Runner,N 个模块 DLL
PowerToys 采用「宿主程序 + 可插拔模块」的架构。每个 PowerToys 工具(PowerToy)都以一个模块接口 DLL 的形式存在,DLL 通过统一接口与 PowerToys.exe(Runner)交互。Runner 承担四项职责(见 architecture.md 与 runner.md):
- 加载各个 PowerToys 模块;
- 将注册的事件(热键按下等)分发给对应的 PowerToy;
- 提供系统托盘图标以管理所有 PowerToys;
- 在 PowerToys 模块与 Settings 设置编辑器之间做桥梁。
模块接口定义的内容包括:
- 热键(hotkey)的数据结构;
- 工具的显示名称(name)与非本地化标识(key);
- 配置管理(get_config / set_config);
- 启用/禁用功能;
- 遥测(Telemetry)上报钩子;
- 组策略对象(GPO)配置读取。
从接口头文件 src/modules/interface/powertoy_module_interface.h 的注释可以看到完整的运行时契约:Runner 对每个 PowerToy DLL 依次执行「加载 DLL → 调用 powertoy_create() 创建对象 → 调用 get_key() 获取非本地化 ID → 调用 enable() 初始化 → 调用 get_hotkeys() 注册热键」;运行期间则按需调用 disable()/enable()/is_enabled()、get_config()、set_config()、call_custom_action()、on_hotkey();退出时先 destroy() 释放全部内存,再卸载 DLL。
值得注意的一个契约细节:Runner 调用 on_hotkey() 时即使模块处于禁用状态也会调用(见头文件第 34 行注释),这为模块提供了「热键触发但功能未启用」时的降级处理空间。
二、四类模块:按功能形态划分的模块类型
architecture.md 将模块分为四类,分别对应不同的实现复杂度:
- 简单模块(Simple Modules)——如 Mouse Pointer Crosshairs、Find My Mouse。整个功能完全包含在模块接口 DLL 内部,不启动任何外部应用程序,例如 Mouse Pointer Crosshairs 直接实现模块接口。
- 外部应用启动器(External Application Launchers)——如 Color Picker。Runner 只负责在热键按下时启动一个独立应用(例如 C# 编写的 WPF 应用),模块与外部应用之间通过命名管道(named pipes)或其他 IPC 机制通信。
- 上下文处理器模块(Context Handler Modules)——如 Power Rename。本质是资源管理器(File Explorer)的 Shell 扩展,通过注册右键上下文菜单入口工作,在 Windows 11 上还通过 MSIX 集成新版上下文菜单。
- 基于注册表的模块(Registry-based Modules)——如 Power Preview。需要在启用/禁用期间修改注册表键值,注册预览处理器(preview handlers)和缩略图提供程序(thumbnail providers)。
这四类的划分对二次开发有直接指导意义:如果你的功能只依赖全局热键和自绘界面,写一个简单模块即可;如果需要独立的现代化 UI 窗口,通常采用外部应用 + IPC 的模式;而需要嵌入资源管理器交互或系统级预览能力的功能,则必须走 Shell 扩展 / 注册表路径。
三、模块接口详解:PowertoyModuleIface
所有 PowerToy 必须实现 powertoy_module_interface.h 中定义的 PowertoyModuleIface 抽象类(接口规范另见 doc/devdocs/modules/interface.md)。核心方法如下表:
| 方法 | 签名要点 | 说明 |
|---|---|---|
get_name() |
const wchar_t* |
返回本地化显示名称 |
get_key() |
const wchar_t* |
返回非本地化 ID,Runner 会将其缓存 |
get_config(buffer, buffer_size) |
返回 bool |
填充可用配置项;若 buffer 为空或太小,把所需大小写回 buffer_size 并返回 false(典型的两段式缓冲区协议) |
set_config(config) |
传入 JSON 宽字符串 | 用户在设置中修改配置后由 Runner 调用,模块应在此持久化设置 |
call_custom_action(action) |
虚函数,有空实现 | 响应用户在设置页点击的自定义动作,可拉起模块自有的编辑器 |
enable() / disable() |
纯虚 | 启用/禁用;disable() 应释放尽可能多的内存 |
is_enabled() |
纯虚 | 返回当前启用状态 |
destroy() |
纯虚 | 销毁对象并释放全部内存 |
get_hotkeys(buffer, size) |
返回 size_t |
返回热键数量并填充缓冲区,默认实现返回 0,模块可覆写 |
on_hotkey(hotkeyId) |
返回 bool |
已注册热键被按下时调用;返回 true 表示该按键应被吞掉 |
GetHotkeyEx() / OnHotkeyEx() |
默认空实现 | 扩展热键形态(modifiersMask + vkCode) |
send_settings_telemetry() |
默认空实现 | 由 Runner 周期性触发模块级设置遥测上报 |
is_enabled_by_default() |
默认 true | 声明模块是否默认启用 |
gpo_policy_enabled_configuration() |
返回 GPO 规则枚举 | 提供模块的 GPO 策略配置值,默认「未配置」 |
热键用两个结构体描述(powertoy_module_interface.h#L41-L86):
Hotkey:win/ctrl/shift/alt四个修饰键布尔量 +unsigned char key+ 模块内标识id(其顺序必须与设置中的顺序一致)+isShown(目前仅 AdvancedPaste 用它决定热键是否在设置中显示)。结构体重载了operator<=>和operator==,便于冲突检测与去重。HotkeyEx:使用WORD modifiersMask与WORD vkCode的扩展形态,配合GetHotkeyEx()/OnHotkeyEx()支持更灵活的修饰键组合。
工厂函数约定(powertoy_module_interface.h#L176-L191):DLL 必须以 powertoy_create() 导出一个工厂函数,示例:
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
Runner 只调用一次 powertoy_create(),之后才有 destroy();返回的对象必须处于禁用状态,由 Runner 决定何时调用 enable() 启动;出错时返回 nullptr。
此外接口还保留了两个特殊约定:
WIN_KEY_HOLD_HOTKEY_ID(等于static_cast<size_t>(-1)):保留 ID,用于旧版「按住 Win 键触发 Shortcut Guide」的on_hotkey调用路径。keep_track_of_pressed_win_key()/milliseconds_win_key_must_be_pressed():旧版 Win 键长按行为的开关与延迟毫秒数,头文件明确提示「新模块不要使用这两个方法」。CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG = 0x110:模块动作(如 AdvancedPaste)会生成新的输入,该标志用于防止集中式键盘钩子再次捕获自己产生的输入,取值刻意选在避开 Keyboard Manager 已有标志的空间。
四、Runner:主程序的职责与启动流程
PowerToys Runner 即 PowerToys.exe 工程,位于 src/runner。其进程启动流程(整理自 doc/devdocs/core/runner.md)为:
- 初始化日志;
- 创建单实例互斥量(single instance mutex);
- 初始化公共工具代码;
- 解析命令行参数;
- 启动托盘图标;
- 初始化低层键盘钩子(low-level keyboard hook);
- 从 DLL 加载各模块接口;
- 启动所有已启用模块;
- 进入 Windows 消息循环;
- 退出时停止各模块并清理资源。
模块加载的具体步骤是:扫描模块目录中的 DLL → 为每个模块创建接口对象 → 加载该模块的设置 → 初始化模块 → 检查 GPO 策略判断哪些模块允许启动 → 启动「已启用且未被策略禁用」的模块。
关键文件与职责:
| 文件 | 职责 |
|---|---|
| src/runner/main.cpp | 程序入口:初始化单例、扫描 ./modules 目录加载所有 PowerToy,对 %LOCALAPPDATA%\Microsoft\PowerToys\settings.json 中标记为启用的模块调用 enable(),随后运行托盘 UI 的消息循环 |
| src/runner/powertoy_module.h / src/runner/powertoy_module.cpp | PowertoyModule 是 PowertoyModuleIface 指针的 RAII 风格持有者,该指针来自调用模块 DLL 的 powertoy_create() |
| src/runner/tray_icon.cpp | 托盘图标与菜单命令管理;通过 start_tray_icon() 创建窗口并注册窗口过程,用 NIF 相关的 shell_notify_icon 注册到系统托盘,处理单击/双击/右键,并监控任务栏重建事件重新注册图标 |
| src/runner/settings_window.cpp | 启动并管理 Settings 窗口进程,通过 Windows 命名管道传输 JSON 消息 |
| src/runner/centralized_hotkeys.cpp | 热键的注册与注销逻辑 |
| src/runner/centralized_kb_hook.cpp | 集中式键盘钩子,为多个模块统一处理热键 |
| src/runner/general_settings.cpp | 通用设置的加载、保存与应用 |
| src/runner/settings_telemetry.cpp | 周期性触发模块级设置遥测的投递、定时与错误处理 |
| src/runner/UpdateUtils.cpp | 自动更新检查、通知与安装 |
| src/runner/auto_start_helper.cpp | 注册/注销登录自启 |
| src/runner/restart_elevated.cpp | 以不同提升级别重启当前进程 |
Runner 还有几个实现层面的要点:它为 WinUI 3 应用做了特殊处理(放在独立目录);对 DLL 做扁平化(flattening)以保持一致版本;以特定类名创建窗口句柄,并把自己注册为需要窗口句柄的组件的窗口处理者——其他进程正是通过查找该托盘窗口类名并向其发送 WM_CLOSE 等消息来与 Runner 通信。
集中式键盘钩子的性能约束
全局热键不各自挂钩子,而是由 centralized_kb_hook.cpp 中的单一钩子统一处理,以避免每个模块各自 SetWindowsHookEx 带来的性能问题。由于该钩子在每次击键时都会被调用,处理函数必须极快,源码中做了多项提前退出的优化:
- 忽略由 PowerToys 自身生成的按键(配合上文提到的
0x110不触发标志); - 在没有实际按键按下时直接返回;
- 利用元数据避免对已处理的修饰键组合重复处理。
五、Settings v2:Runner 与设置系统之间的桥
Runner 是模块与 Settings 编辑器之间的桥梁。当前实现为 Settings v2,其架构、ViewModel、IPC、GPO 集成、遥测等细节在 doc/devdocs/core/settings/readme.md 中分篇展开。与本文主题直接相关的通信机制是:
- Runner 与 Settings UI 是两个进程,通过 Windows 命名管道传输 JSON 消息;
- 双向管道由公共库中的
TwoWayPipeMessageIPC实现,C++ 端头文件为 src/common/interop/two_way_pipe_message_ipc.h,托管侧封装为 src/common/interop/TwoWayPipeMessageIPCManaged.cpp; - 例如托盘左键单击显示快捷浮窗时,Runner 通过管道发送
{"ShowYourself":"flyout"},打开主面板则发送{"ShowYourself":"Dashboard"},还可附带x_position/y_position坐标让浮窗出现在托盘图标附近。
六、公共依赖与跨模块复用库
architecture.md 列出的公共依赖及其在仓库中的落点:
| 依赖/库 | 用途 | 仓库位置 |
|---|---|---|
| SPDLOG | C++ 集中式日志系统 | 通过 vcpkg 管理,版本与补丁见 deps/vcpkg-overlays/spdlog/portfile.cmake、vcpkg-configuration.json 与 vcpkg.json |
| Cpp WinRT | 被大多数工具使用的 WinRT 互操作 | 各模块 .vcxproj 引用(CMake/MSBuild 属性统一配置) |
common 公共库 |
跨模块复用的工具代码,例如 JSON 解析 与 IPC 原语 | src/common |
| Interop 库 | C++ 与 C# 的通信,已转换为 C++ WinRT 形式 | src/common/interop,包含 HotkeyManager、KeyboardHook、TwoWayPipeMessageIPCManaged 等 .idl 投影 |
| Common.UI | 带 WPF 与 WinForms 依赖的公共 UI 库 | src/common/Common.UI |
从 src/common/interop 的文件构成(*.idl + *.cpp + .def 导出)可以推断,该库通过 C++/WinRT 投影向 C# 侧暴露钩子、热键管理、双向管道等能力,这正是「Interop library for C++/C# communication(converted to C++ WinRT)」这句文档描述的实现形态。更完整的公共库清单参见 doc/devdocs/common/readme.md。
七、资源管理:.resx、.rc 与 PRI 命名
文档明确了三类应用的资源处理方式差异:
- C++ 应用与模块接口:
.resx资源文件需要转换为.rc才能在构建中被资源编译器消费,构建前需使用转换工具(这也是 C++ 模块工程中普遍存在*.rc+resources.rc生成文件的原因); - WPF 应用:直接使用
.resx文件; - WinUI 3 应用:使用
.resw文件。
另外一个容易踩坑的约束是 PRI 文件命名:MSIX/WinUI 包在扁平化(flattening)过程中会合并多个应用资源,若各模块使用默认 PRI 名称会发生冲突,因此必须覆写默认名称加以区分。
八、从模板开始写一个新模块
头文件注释直接指向了官方起步模板:tools/project_template/ModuleTemplate(见 src/modules/interface/powertoy_module_interface.h#L10)。该模板位于 tools/project_template/ModuleTemplate,提供了一个最简的、无操作的 PowerToy 实现,包含 DLL 工程、powertoy_create() 导出与 PowertoyModuleIface 骨架。按本文第三节的方法契约实现 enable()/disable()/get_config()/set_config()/get_hotkeys()/on_hotkey() 等成员,并在 get_key() 中返回全局唯一标识,即得到一个可被 Runner 扫描加载的模块。
九、小结:架构约束换来的可维护性
PowerToys 的架构可以用三条主线概括:
- 接口即契约:
PowertoyModuleIface+powertoy_create()工厂导出把「Runner 能做什么、模块要提供什么」固化在一个头文件里,Runner 生命周期调用序列(create → get_key → enable → get_hotkeys → on_hotkey/set_config → destroy)完全由注释与实现双向约定; - 进程边界清晰:Runner(C++ 主程序)、Settings v2(独立 UI 进程)、外部工具应用(WPF/WinUI 3)之间只通过命名管道 + JSON 消息交互,GPO 与单实例互斥量则由 Runner 统一把关;
- 复用下沉到 common:日志(SPDLOG)、IPC 原语、JSON、C++/C# 互操作(WinRT 投影)、UI 主题等全部下沉到 src/common,模块只写业务逻辑。
理解这套机制后,无论是排查「模块为何没有被启用」(检查 settings.json 与 GPO 策略两道关卡)、「热键为何不触发」(集中式钩子的提前退出条件与 0x110 自触发标志),还是新增一个工具(从 ModuleTemplate 起步实现接口契约),都能在 src/runner、src/modules/interface/powertoy_module_interface.h 与 doc/devdocs/core 文档中找到直接依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00