首页
/ PowerToys 模块接口(PowertoyModuleIface)详解:从 DLL 加载到配置、热键的完整生命周期

PowerToys 模块接口(PowertoyModuleIface)详解:从 DLL 加载到配置、热键的完整生命周期

2026-09-04 14:51:27作者:咎竹峻Karen

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.dllPowerToys.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)();

需要注意两点演进:

  1. 文档代码清单中的 get_events() 是历史接口(用于声明模块支持的事件),在当前 powertoy_module_interface.h 中已不再存在,文档保留的是接口早期形态的快照。
  2. 当前头文件在文档所述方法基础上增加了非本地化键 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() 初始化模块。

运行阶段(在 createdestroy() 之间可能随时发生):

  • 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 的两条契约:

  1. 必须从 DLL 导出名为 powertoy_create 的符号,Runner 通过 GetProcAddress 定位;
  2. 出错时返回 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.cppmodules().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 thisdllmain.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 是理解整套接口的最佳范本,完整演示了:

  1. 实现 PowertoyModuleIface 全部纯虚函数(get_name / get_key / get_config / set_config / enable / disable / is_enabled / destroy);
  2. 构造函数中调用 init_settings(),用 PowerToyValues::load_from_settings_file(get_key()) 从持久化文件恢复上次保存的设置,解析失败时保留默认值;
  3. get_config() 中用 Settings 对象声明各类设置项(模板注释列出了 bool 开关、int 自旋框、string、color picker、custom action 五种编辑器的添加方式);
  4. set_config() 中解析 JSON、更新内存设置并 save_to_settings_file() 落盘;
  5. 末尾以 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.hupdate_hotkeys()
需要策略管控时实现 gpo_policy_enabled_configuration() powertoy_module_interface.h

八、关键文件索引

理解并实现这套接口后,就可以把握 PowerToys 插件机制的核心:模块侧只需专注实现一个 C++ 类和一个导出函数,加载、启用/禁用、配置持久化、热键分发、策略管控等宿主职责全部由 Runner 按既定生命周期代劳。

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