PowerToys 模块接口(PowertoyModuleIface)详解:从 DLL 加载到配置、热键的完整生命周期
PowerToys 采用"主进程 Runner + 各功能模块 DLL"的插件式架构,每个 PowerToy 本质上是一个导出 powertoy_create() 工厂函数的动态库,通过 接口定义文档 约定的 PowertoyModuleIface 抽象接口与 Runner 通信。本文基于该接口文档,结合当前仓库中 接口头文件、Runner 加载实现 和 官方模块模板,完整讲解模块的生命周期、每个接口语义及其底层调用链,帮助读者既能读懂既有模块的实现,也能独立开发一个新的 PowerToys 模块。
一、接口定位:Runner 与模块之间的 DLL 契约
PowerToys 的各个功能(Color Picker、Keyboard Manager、FancyZones 等)都以独立的 DLL 形式存在,编译后按固定名称放置在 Runner 目录下的已知模块清单中。src/runner/main.cpp 中维护着 knownModules 列表(如 PowerToys.ZoomItModuleInterface.dll、PowerToys.CmdPalModuleInterface.dll 等),Runner 启动时逐一加载。
模块与宿主之间唯一的约定就是 src/modules/interface/powertoy_module_interface.h 中定义的 PowertoyModuleIface 类和 powertoy_create_func 工厂函数指针类型。头文件开头的注释明确了契约内容:DLL 中的 powertoy_create() 必须返回一个实现该接口的对象,Runner 加载 DLL 后调用该函数创建模块,随后调用 get_key() 获取非本地化 ID、调用 enable() 初始化模块,并在运行期间按需调用配置与热键相关方法。
二、接口定义全貌
接口文档给出的经典接口定义为:
class PowertoyModuleIface {
public:
virtual const wchar_t* get_name() = 0;
virtual const wchar_t** get_events() = 0;
virtual bool get_config(wchar_t* buffer, int *buffer_size) = 0;
virtual void set_config(const wchar_t* config) = 0;
virtual void call_custom_action(const wchar_t* action) {};
virtual void enable() = 0;
virtual void disable() = 0;
virtual bool is_enabled() = 0;
virtual void destroy() = 0;
};
typedef PowertoyModuleIface* (__cdecl *powertoy_create_func)();
需要注意两点演进:
- 文档代码清单中的
get_events()是历史接口(用于声明模块支持的事件),在当前 powertoy_module_interface.h 中已不再存在,文档保留的是接口早期形态的快照。 - 当前头文件在文档所述方法基础上增加了非本地化键
get_key()、热键注册get_hotkeys()/on_hotkey()、扩展热键GetHotkeyEx()/OnHotkeyEx()、遥测与 GPO 策略查询等方法。这些扩展方法大多带有默认实现,模块可以按需覆写。
当前接口的核心成员如下(摘自 powertoy_module_interface.h):
class PowertoyModuleIface
{
public:
/* Returns the localized name of the PowerToy*/
virtual const wchar_t* get_name() = 0;
/* Returns non localized name of the PowerToy, this will be cached by the runner. */
virtual const wchar_t* get_key() = 0;
/* Fills a buffer with the available configuration settings. ... */
virtual bool get_config(wchar_t* buffer, int* buffer_size) = 0;
/* Sets the configuration values. */
virtual void set_config(const wchar_t* config) = 0;
/* Call custom action from settings screen. */
virtual void call_custom_action(const wchar_t* /*action*/){};
/* Enables the PowerToy. */
virtual void enable() = 0;
/* Disables the PowerToy, should free as much memory as possible. */
virtual void disable() = 0;
/* Should return if the PowerToys is enabled or disabled. */
virtual bool is_enabled() = 0;
/* Destroy the PowerToy and free all memory. */
virtual void destroy() = 0;
/* Get the list of hotkeys. ... 默认返回 0,模块无需覆写 */
virtual size_t get_hotkeys(Hotkey* /*buffer*/, size_t /*buffer_size*/) { return 0; }
virtual bool on_hotkey(size_t /*hotkeyId*/) { return false; }
virtual void send_settings_telemetry() {}
virtual bool is_enabled_by_default() const { return true; }
/* 查询 GPO 策略对该模块启用状态的影响 */
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration()
{
return powertoys_gpo::gpo_rule_configured_not_configured;
}
};
接口内还内嵌了一个描述热键的 Hotkey 结构体(powertoy_module_interface.h):
struct Hotkey
{
bool win = false;
bool ctrl = false;
bool shift = false;
bool alt = false;
unsigned char key = 0;
// id 用于在模块内标识热键;模块接口中的顺序应与设置页中的顺序一致
int id = 0;
// 目前仅用于 AdvancedPaste 判断热键是否在设置中显示
bool isShown = true;
};
id 与热键在设置页中的顺序保持一致这一约定很关键:Runner 回调 on_hotkey(hotkeyId) 时传入的正是模块自己声明的 id,模块据此区分是哪条快捷键被触发。
三、运行时逻辑:模块的完整生命周期
接口文档描述了 Runner 对每个 PowerToy DLL 的完整处理流程:
启动阶段(针对每个模块 DLL):
- 加载 DLL;
- 调用
powertoy_create()创建 PowerToy 对象; - 对返回对象依次调用
get_name()(当前实现中为get_key())获取名称、enable()初始化模块。
运行阶段(在 create 与 destroy() 之间可能随时发生):
disable()/enable()/is_enabled():切换或查询启用状态;get_config():获取可用配置项;set_config():用户在设置编辑器中修改配置后,将新值传递给模块;call_custom_action():用户在设置编辑器中点击自定义操作按钮时触发。
退出阶段:
- 调用
disable(); - 调用
destroy(),应释放全部内存并删除 PowerToy 对象; - 卸载 DLL。
当前头文件的注释(powertoy_module_interface.h)对运行与退出阶段的描述略有精简,强调 Runner 还会在设置变更时再次调用 get_hotkeys() 保持热键最新,并在对应快捷键按下时回调 on_hotkey()——甚至模块被禁用时也会回调 on_hotkey(),由模块自行决定是否忽略。
Runner 侧的加载实现
上述流程在源码中的落点是 src/runner/powertoy_module.cpp:
PowertoyModule load_powertoy(const std::wstring_view filename)
{
auto handle = winrt::check_pointer(LoadLibraryW(filename.data()));
auto create = reinterpret_cast<powertoy_create_func>(GetProcAddress(handle, "powertoy_create"));
if (!create)
{
FreeLibrary(handle);
winrt::throw_last_error();
}
auto pt_module = create();
if (!pt_module)
{
FreeLibrary(handle);
winrt::throw_hresult(winrt::hresult(E_POINTER));
}
return PowertoyModule(pt_module, handle);
}
这里印证了文档中 powertoy_create_func 的两条契约:
- 必须从 DLL 导出名为
powertoy_create的符号,Runner 通过GetProcAddress定位; - 出错时返回
nullptr——Runner 会立刻FreeLibrary卸载 DLL 并抛出异常。
PowertoyModule 类(src/runner/powertoy_module.h)用两个自定义删除器保证资源成对释放:PowertoyModuleDeleter 在对象析构时调用 destroy(),PowertoyModuleDLLDeleter 在 HMODULE 释放时调用 FreeLibrary。也就是说,只要 PowertoyModule 离开作用域,"先 destroy 再卸载 DLL"的退出顺序就自动成立。模块的 get_key() 返回值被用作 模块注册表 这个全局 std::map<std::wstring, PowertoyModule> 的键,main.cpp 中 modules().emplace(pt_module->get_key(), std::move(pt_module)) 一行完成了登记。
此外,PowertoyModule 的构造函数中还会执行 remove_hotkey_records()、update_hotkeys() 和 UpdateHotkeyEx()(powertoy_module.cpp),即模块刚被创建时就会把其声明的热键注册进中央键盘钩子——这解释了文档中"运行期间 Runner 可能在任意时刻调用 get_hotkeys()"的语义:设置里热键一变化,Runner 就会重新拉取并刷新注册。
四、各方法详解
powertoy_create_func
typedef PowertoyModuleIface* (__cdecl *powertoy_create_func)()
创建 PowerToy 对象的工厂函数指针类型。契约要点(与文档一致):
- 必须由 DLL 以
powertoy_create()名称导出,推荐写法:
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new MyPowertoy();
}
- 由 Runner 调用以初始化每个 PowerToy,且在
destroy()之前只会被调用一次; - 返回的 PowerToy 应处于禁用状态,Runner 随后会调用
enable()使其启动; - 出错时返回
nullptr。
get_name 与 get_key
virtual const wchar_t* get_name() = 0; // 本地化显示名称
virtual const wchar_t* get_key() = 0; // 非本地化键,Runner 会缓存
文档中的 get_name() 返回 PowerToy 名称且会被 Runner 缓存。当前实现进一步区分了"显示名"(get_name(),本地化,展示在设置页)与"键"(get_key(),非本地化,作为模块注册表、设置 JSON、热键注册中的唯一标识)。从源码结构看,get_key() 承担了文档中 get_name() 的缓存职责,get_name() 退化为纯展示用途。
get_config
virtual bool get_config(wchar_t* buffer, int* buffer_size)
向缓冲区填充模块可用的配置项(序列化的 JSON 字符串)。协议采用经典的"两阶段"模式:
- 若
buffer为 nullptr 或buffer_size不够大,把所需缓冲区大小写入*buffer_size并返回false; - 缓冲区足够时写入内容并返回
true。
Runner 侧的消费逻辑在 PowertoyModule::json_config() 中,可以逐行对照这一协议:
json::JsonObject PowertoyModule::json_config() const
{
int size = 0;
pt_module->get_config(nullptr, &size); // 第一阶段:询问所需大小
std::wstring result;
result.resize(static_cast<size_t>(size) - 1);
pt_module->get_config(result.data(), &size); // 第二阶段:实际填充
return json::JsonObject::Parse(result);
}
模块实现时通常借助 SettingsAPI 提供的 PowerToysSettings::Settings 对象来声明配置项(bool 开关、int 自旋框、字符串、颜色选择器、自定义操作等),最后调用 serialize_to_buffer(buffer, buffer_size) 完成填充,官方模板 ModuleTemplate/dllmain.cpp 给出了完整注释版本。
set_config
virtual void set_config(const wchar_t* config)
用户在设置编辑器中修改模块设置后,Runner 调用此方法把更新值(JSON 字符串)传给模块。文档特别指出这是保存设置的合适位置。调用链在 src/runner/settings_window.cpp 中:send_json_config_to_module() 找到目标模块后调用 set_config();如果本次变更涉及热键(hotkeyUpdated),还会紧接着执行 remove_hotkey_records()、update_hotkeys()、UpdateHotkeyEx() 刷新快捷键注册。
模板实现中推荐用 PowerToysSettings::PowerToyValues::from_json_string(config, get_key()) 解析 JSON,逐项更新内存中的设置对象,然后调用 values.save_to_settings_file() 持久化(见 dllmain.cpp);若需要对设置做自定义处理,可先自行解析再落盘。
call_custom_action
virtual void call_custom_action(const wchar_t* action)
响应用户在设置编辑器中点击"自定义操作"按钮的回调,可用于启动由 PowerToy 自定义的复杂编辑器(如 Keyboard Manager 的按键映射编辑器、FancyZones 的布局编辑器)。注意接口中该方法带有空默认实现(非纯虚),即没有自定义操作的模块无需覆写。
参数 action 是一个 JSON 字符串,模板中的解析方式为:
PowerToysSettings::CustomActionObject action_object =
PowerToysSettings::CustomActionObject::from_json_string(action);
// 按 action_object.get_name() 分发到具体处理逻辑
Runner 侧触发点在 settings_window.cpp:设置界面回传的 JSON 中,若某元素属于已注册模块,就取其 Stringify() 结果调用 modules().at(name)->call_custom_action(element.c_str())。
enable / disable / is_enabled
virtual void enable() = 0; // 启用 PowerToy
virtual void disable() = 0; // 禁用 PowerToy,应释放尽可能多的内存
virtual bool is_enabled() = 0; // 返回当前状态
文档的要点:disable() 不只是翻转状态位,还应释放尽可能多的内存——因为模块被禁用后可能长期驻留但不工作。Runner 根据用户在设置页的切换调用它们,src/runner/general_settings.cpp 中可以看到 powertoy->enable() / powertoy->disable() 的直接调用点(如模块启用状态恢复、按 GPO 策略修正状态等场景)。
destroy
virtual void destroy()
销毁 PowerToy 并释放全部内存。契约是"销毁并释放所有内存",因此模板实现中 destroy() 直接 delete this(dllmain.cpp):对象自毁,Runner 通过 unique_ptr 的自定义删除器触发这一调用。实现模块时应确保 disable() 阶段已解除钩子、定时器、注册表监听等资源,destroy() 阶段只做收尾,避免与仍在运行的回调竞争。
五、当前接口的扩展能力:热键与系统集成
文档描述的是接口"经典骨架",当前头文件在此之上扩展了三类能力,了解它们能完整解释 Runner 与模块的交互:
1. 集中式热键注册
模块覆写 get_hotkeys(buffer, buffer_size) 返回热键数量并填充 Hotkey 数组,Runner 在 update_hotkeys() 中把它们注册进 HotkeyConflictDetector 冲突检测器和 CentralizedKeyboardHook 中央键盘钩子,并为每个 isShown 的热键绑定回调 modulePtr->on_hotkey(i)。返回 true 表示该按键事件被模块"吞掉"。UpdateHotkeyEx()(powertoy_module.cpp)则处理 GetHotkeyEx() 返回的扩展热键,并保留了 Shortcut Guide "按住 Windows 键"的遗留行为(通过 keep_track_of_pressed_win_key() / milliseconds_win_key_must_be_pressed() 控制,头文件明确提示新模块不要使用这两个遗留方法)。
2. GPO 组策略集成
gpo_policy_enabled_configuration() 允许模块上报组策略对其启用状态的约束(返回 not_configured / 强制启用 / 强制禁用),Runner 据此阻止用户手动违背策略。配合仓库中的 GPOWrapper 公共组件 与 gpo 资产目录,管理员可以按模块粒度通过 ADMX 策略统一管控。
3. 其他默认实现
send_settings_telemetry():Runner 统计设置使用情况时的遥测上报钩子;is_enabled_by_default():声明模块默认是否启用,默认为true;CreateDefaultEvent()(powertoy_module_interface.h 的 protected 成员):辅助模块创建命名事件对象,便于同一模块的多个进程间同步状态。
六、实战参考:官方模块模板
仓库提供了最小可运行的模块骨架 tools/project_template/ModuleTemplate,其 dllmain.cpp 是理解整套接口的最佳范本,完整演示了:
- 实现
PowertoyModuleIface全部纯虚函数(get_name/get_key/get_config/set_config/enable/disable/is_enabled/destroy); - 构造函数中调用
init_settings(),用PowerToyValues::load_from_settings_file(get_key())从持久化文件恢复上次保存的设置,解析失败时保留默认值; get_config()中用Settings对象声明各类设置项(模板注释列出了 bool 开关、int 自旋框、string、color picker、custom action 五种编辑器的添加方式);set_config()中解析 JSON、更新内存设置并save_to_settings_file()落盘;- 末尾以
extern "C" __declspec(dllexport)导出powertoy_create()。
该模板还可通过 Visual Studio 的"导出模板"机制生成名为 PowerToys Module 的项目模板(README 给出完整的 .vstemplate XML 配置),供后续模块开发直接复用。
七、模块实现要点清单
综合文档契约与源码事实,实现一个合格的 PowerToys 模块需满足:
| 要求 | 依据 |
|---|---|
导出 powertoy_create(),返回禁用状态的对象;失败返回 nullptr |
接口文档;load_powertoy() |
get_name() / get_key() 返回的指针必须长期有效(Runner 会缓存 get_key()) |
powertoy_module_interface.h |
get_config() 严格遵循"先问大小、再填内容"的两阶段协议 |
json_config() |
set_config() 是持久化设置的入口,解析失败要容错(模板中 catch 住异常) |
send_json_config_to_module() |
disable() 尽量释放内存;destroy() 保证全量释放且对象自毁 |
接口文档;PowertoyModuleDeleter |
使用热键时覆写 get_hotkeys(),并在 on_hotkey(id) 中按 id 分发;热键顺序须与设置页一致 |
powertoy_module_interface.h;update_hotkeys() |
需要策略管控时实现 gpo_policy_enabled_configuration() |
powertoy_module_interface.h |
八、关键文件索引
- 接口文档:doc/devdocs/modules/interface.md
- 接口定义:src/modules/interface/powertoy_module_interface.h
- Runner 模块加载与热键注册:src/runner/powertoy_module.h、src/runner/powertoy_module.cpp
- 模块清单与启动加载:src/runner/main.cpp
- 设置分发(set_config / call_custom_action):src/runner/settings_window.cpp
- 启用状态切换:src/runner/general_settings.cpp
- 模块模板:tools/project_template/ModuleTemplate/dllmain.cpp、tools/project_template/ModuleTemplate/README.md
- 配置序列化组件:src/common/SettingsAPI/settings_objects.h
理解并实现这套接口后,就可以把握 PowerToys 插件机制的核心:模块侧只需专注实现一个 C++ 类和一个导出函数,加载、启用/禁用、配置持久化、热键分发、策略管控等宿主职责全部由 Runner 按既定生命周期代劳。
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 StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00