首页
/ PowerToys 核心架构解析:Runner、模块接口与设置系统的协作机制

PowerToys 核心架构解析:Runner、模块接口与设置系统的协作机制

2026-09-06 19:26:59作者:邵娇湘

本篇基于 PowerToys 官方开发文档 doc/devdocs/core/architecture.md,系统讲解 PowerToys 的三层核心架构——PowerToys Runner(主程序)、标准化的模块接口(Module Interface DLL)以及 Settings 设置系统,并结合 src/modules/interface/powertoy_module_interface.hsrc/runner 等真实源码,说明每个设计决策背后的实现依据。读完本文,你将能够回答:一个新的 PowerToy 模块如何被 Runner 加载、启用、接收热键和配置变更,以及模块与设置 UI 之间如何跨进程通信。

一、架构总览:一个 Runner,N 个模块 DLL

PowerToys 采用「宿主程序 + 可插拔模块」的架构。每个 PowerToys 工具(PowerToy)都以一个模块接口 DLL 的形式存在,DLL 通过统一接口与 PowerToys.exe(Runner)交互。Runner 承担四项职责(见 architecture.mdrunner.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 将模块分为四类,分别对应不同的实现复杂度:

  1. 简单模块(Simple Modules)——如 Mouse Pointer Crosshairs、Find My Mouse。整个功能完全包含在模块接口 DLL 内部,不启动任何外部应用程序,例如 Mouse Pointer Crosshairs 直接实现模块接口。
  2. 外部应用启动器(External Application Launchers)——如 Color Picker。Runner 只负责在热键按下时启动一个独立应用(例如 C# 编写的 WPF 应用),模块与外部应用之间通过命名管道(named pipes)或其他 IPC 机制通信。
  3. 上下文处理器模块(Context Handler Modules)——如 Power Rename。本质是资源管理器(File Explorer)的 Shell 扩展,通过注册右键上下文菜单入口工作,在 Windows 11 上还通过 MSIX 集成新版上下文菜单。
  4. 基于注册表的模块(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):

  • Hotkeywin/ctrl/shift/alt 四个修饰键布尔量 + unsigned char key + 模块内标识 id(其顺序必须与设置中的顺序一致)+ isShown(目前仅 AdvancedPaste 用它决定热键是否在设置中显示)。结构体重载了 operator<=>operator==,便于冲突检测与去重。
  • HotkeyEx:使用 WORD modifiersMaskWORD 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)为:

  1. 初始化日志;
  2. 创建单实例互斥量(single instance mutex);
  3. 初始化公共工具代码;
  4. 解析命令行参数;
  5. 启动托盘图标;
  6. 初始化低层键盘钩子(low-level keyboard hook);
  7. 从 DLL 加载各模块接口;
  8. 启动所有已启用模块;
  9. 进入 Windows 消息循环;
  10. 退出时停止各模块并清理资源。

模块加载的具体步骤是:扫描模块目录中的 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 PowertoyModulePowertoyModuleIface 指针的 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.cmakevcpkg-configuration.jsonvcpkg.json
Cpp WinRT 被大多数工具使用的 WinRT 互操作 各模块 .vcxproj 引用(CMake/MSBuild 属性统一配置)
common 公共库 跨模块复用的工具代码,例如 JSON 解析IPC 原语 src/common
Interop 库 C++ 与 C# 的通信,已转换为 C++ WinRT 形式 src/common/interop,包含 HotkeyManagerKeyboardHookTwoWayPipeMessageIPCManaged.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 的架构可以用三条主线概括:

  1. 接口即契约PowertoyModuleIface + powertoy_create() 工厂导出把「Runner 能做什么、模块要提供什么」固化在一个头文件里,Runner 生命周期调用序列(create → get_key → enable → get_hotkeys → on_hotkey/set_config → destroy)完全由注释与实现双向约定;
  2. 进程边界清晰:Runner(C++ 主程序)、Settings v2(独立 UI 进程)、外部工具应用(WPF/WinUI 3)之间只通过命名管道 + JSON 消息交互,GPO 与单实例互斥量则由 Runner 统一把关;
  3. 复用下沉到 common:日志(SPDLOG)、IPC 原语、JSON、C++/C# 互操作(WinRT 投影)、UI 主题等全部下沉到 src/common,模块只写业务逻辑。

理解这套机制后,无论是排查「模块为何没有被启用」(检查 settings.json 与 GPO 策略两道关卡)、「热键为何不触发」(集中式钩子的提前退出条件与 0x110 自触发标志),还是新增一个工具(从 ModuleTemplate 起步实现接口契约),都能在 src/runnersrc/modules/interface/powertoy_module_interface.hdoc/devdocs/core 文档中找到直接依据。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391