首页
/ PowerToys 源码架构指南:从模块 DLL、Runner 到设置的工程组织解析

PowerToys 源码架构指南:从模块 DLL、Runner 到设置的工程组织解析

2026-09-06 18:25:02作者:何将鹤

本文是一份面向开发者的 PowerToys 源码导读。仓库根目录下的 src/README.md 用短短几行勾勒出源码的顶层组织——每个 PowerToy 模块是一个 DLL、由一个可执行文件统一加载管理、设置窗口与共享代码库各自独立——本文以该文档为核心骨架,结合 runnermodulessettings-uicommon 四个目录的真实实现,逐层还原这套模块化架构的加载链、生命周期与构建入口,帮助读者快速定位代码、理解"新增一个 PowerToy"或"排查某个模块问题"时应该从哪里入手。

一、源码组织总览:四个顶层目录的分工

根据 src/README.md 的描述,PowerToys 的源码被清晰地划分为四个部分:

目录 角色 产出物 说明
src/modules PowerToy 功能模块 一组 DLL 每个 PowerToy 拆分到独立 DLL,便于独立加载与隔离
src/runner 宿主进程 PowerToys.exe(可执行文件) 负责加载并管理上述所有模块 DLL
src/settings-ui 设置程序 独立可执行文件 与 runner 分离,承载全部用户配置界面
src/common 共享代码 静态库/头文件集合 提供 runner 与各模块共同使用的基础能力

其中最关键的一句是:模块被拆成 DLL,runner 以可执行文件的形式加载并管理这些 DLL。这意味着模块与宿主之间通过稳定的二进制接口解耦——模块可以单独演进、按需加载,宿主进程统一负责启停调度、快捷键注册与全局设置下发。

二、src/modules:每个 PowerToy 一个 DLL

src/modules 目录下除 interface/ 之外,每一个子目录都对应一个 PowerToy(或其模块家族),例如:

窗口与布局类:fancyzonesWorkspacesalwaysontopCropAndLockGrabAndMoveAltWindowCycleShortcutGuide; 输入与效率类:keyboardmanagerlaunchercmdpalcmdpal 之外的 colorPickerpoweraccentPowerOCRpeekAdvancedPasteMouseUtilsMouseWithoutBordersFindMyMouse(位于 MouseUtils 家族)等; 文件与系统类:imageresizerpowerrenamepreviewpane(File Explorer 预览)、FileLocksmithHostsregistrypreviewEnvironmentVariablesNewPlusMeasureTool; 显示与工具类:awakeLightSwitchZoomItpowerdisplaycmdNotFoundMonitorReport(tools 下)等。

这些模块的产出物并不只有同一种形态:绝大多数编译为原生 C++ DLL,而部分基于 WinUI 3 的模块(如 PowerToys.ImageResizerExt.dllPowerToys.Peek.dllPowerToys.RegistryPreviewExt.dll)在安装目录下归属于 WinUI3Apps 子目录,这一点可以从 runner 的加载清单中得到印证(详见下文)。

2.1 模块 DLL 的统一接口

src/modules/interface 下存放着所有模块 DLL 必须遵循的 C++ 接口定义 powertoy_module_interface.h。该文件头部的注释完整描述了 runner 与模块之间的调用契约:

  • 加载阶段:runner 依次 LoadLibrary 加载 DLL、调用工厂函数 powertoy_create() 创建 PowerToy 对象,随后调用 get_key() 获取模块的非本地化 ID、enable() 初始化模块、get_hotkeys() 注册模块使用的快捷键;
  • 运行阶段:runner 可能调用 disable()/enable()/is_enabled() 切换状态、get_config()/set_config() 读写配置、call_custom_action() 触发设置页中的自定义动作,以及 on_hotkey() 响应快捷键按下;
  • 退出阶段:runner 调用 destroy() 释放模块内存并卸载 DLL。

核心类 PowertoyModuleIface 定义了这些纯虚方法,其中几个值得关注的点:

  • get_name() 返回本地化显示名,get_key() 返回非本地化 ID(会被 runner 缓存,作为模块在 modules() 映射表中的键);
  • get_hotkeys() 通过"先问大小、再填缓冲"的两段式调用返回 Hotkey 数组,每个 Hotkeywin/ctrl/shift/alt 修饰键位、key 虚拟键码、idisShown 组成;即使模块处于禁用状态,runner 仍会调用它以保证快捷键状态最新;
  • HotkeyExOnHotkeyEx() 是为新一代集中式快捷键机制预留的扩展入口;
  • 工厂函数必须以 extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() 形式从 DLL 导出,创建出的对象初始应处于 disabled 状态,出错时返回 nullptr

需要开发新 PowerToy 时,可以直接以仓库中的 tools/project_template/ModuleTemplate 空实现为起点,它演示了满足上述接口的最小化模块骨架。

三、src/runner:加载与管理所有模块的宿主进程

src/runner 产出 PowerToys.exe。从代码看,它的职责远不止加载模块,还包括托盘图标、开机自启、更新检测、权限提升判断、设置窗口唤起、集中式键盘钩子等系统级事务。本文聚焦于它与模块架构直接相关的部分。

3.1 模块加载的完整调用链

模块的实际加载逻辑集中在 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"));
    ...
    auto pt_module = create();
    ...
    return PowertoyModule(pt_module, handle);
}

即:LoadLibraryW 载入 DLL → GetProcAddress 找到导出的 powertoy_create 工厂 → 调用工厂创建 PowertoyModuleIface 对象。外层包装类 PowertoyModule 用两个 RAII 删除器(PowertoyModuleDeleterPowertoyModuleDLLDeleter)分别管理接口对象的 destroy() 与模块句柄的 FreeLibrary,并在构造时向中央快捷键管理器注册热键。

3.2 runner 的启动主流程

入口在 main.cppWinMain 中,其内部 runner() 函数完成核心启动序列:

  1. 初始化日志、DPI 感知、托盘图标与集中式键盘钩子(CentralizedKeyboardHook::Start());
  2. 依次加载 knownModules 清单中列出的全部模块 DLL(见 main.cpp 的模块清单),清单中既包含 PowerToys.FancyZonesModuleInterface.dllPowerToys.KeyboardManager.dll 这类顶层 DLL,也包含 WinUI3Apps/PowerToys.ImageResizerExt.dll 这类位于子目录的模块,还标注了各个模块的加载路径写法;
  3. 加载成功后以 get_key() 返回值作为键放入全局映射 modules(),随后调用 start_enabled_powertoys() 启动所有处于启用状态的模块;
  4. 处理 --open-settings、OOBE/SCOOBE 欢迎流程、首次运行与版本更新后的提示等分支;
  5. 进入消息循环,退出时依据单例互斥体(MSI mutex)判断是否需要调度进程重启(普通权限/管理员权限切换)。

值得注意的错误处理细节:模块加载失败时,Debug 构建只记录 warning 并继续运行,Release 构建则弹出错误对话框——注释表明这是有意为之,避免开发者必须一次构建通过全部模块才能调试的负担。

3.3 模块生命周期与快捷键管理

模块管理集中在 powertoy_module.cpp

  • update_hotkeys() 通过两段式 get_hotkeys 拿到模块声明的热键,过滤 isShown 后写入全局的 HotkeyConflictDetector::HotkeyConflictManager(用于冲突检测),同时为每个热键注册一个回调,在热键触发时调用 modulePtr->on_hotkey(i)
  • UpdateHotkeyEx() 处理新一代集中式热键(CentralizedHotkeys),并在模块启用且声明 keep_track_of_pressed_win_key() 时,为 Win 键长按这一"遗留行为"注册按下时长动作——接口注释明确不建议新模块使用该路径;
  • json_config() 示范了 get_config 的两段式调用如何把模块配置序列化成 JSON 字符串供上层使用。

四、src/settings-ui:独立于 runner 的设置程序

src/README.md 的说法,设置窗口是独立的可执行文件,与 runner 进程解耦,这保证"改设置"与"运行模块"互不阻塞。

需要向读者澄清的一点是:该 README 描述设置窗口"利用 WebView 渲染 HTML 页面",这与当前仓库的实现已不一致。从 src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs 的源码看,当前设置应用是 WinUI 3 XAML 应用(public partial class App : Application),目录内包含大量 .xaml 页面、.cs 视图模型与配套资源;runner 侧也有 open_settings_windowopen_oobe_windowopen_scoobe_window 等入口负责唤起设置/引导窗口。因此阅读源码时应以 WinUI 3 的现实为准,README 中 WebView 的描述更接近该文件较早时期(或面向非源码读者)的概括。

settings-ui 下的工程结构(Settings.UISettings.UI.LibrarySettings.UI.ControlsQuickAccess.UI 等)同时承载着:

  • 各模块的设置页与通用控件(如快捷键控件);
  • 首次运行的 OOBE 与更新后的 SCOOBE 欢迎视图;
  • 与 runner 共享的设置序列化与深链接能力(如 --open-settings=Overview 打开特定页面)。

五、src/common:runner 与模块共用的基础能力库

src/README.md 的描述,src/common 提供"runner 与所有 PowerToy 模块共同使用的辅助函数静态库"。从目录清单看,这一"库"实际是按用途组织的多组工程与头文件:

  • 运行基础设施interop(跨模块 COM/接口定义)、logger(日志)、SettingsAPI(C++ 侧设置读写与文件监听)、notifications(Windows 通知)、updating(更新检查/安装)、version(版本号);
  • 通用能力utils/(头文件形态的纯工具集合,例如 JSON、GPO、进程、窗口、路径辅助)、monitor_utils.hdebug_control.hhooks/
  • C++ 与 .NET 桥接GPOWrapper(把组策略规则暴露给模块)、GPOWrapperProjection(供 .NET 端调用)、ManagedCommonManagedCsWin32(供 C# 模块使用的 Win32 P/Invoke);
  • 各模块公共支撑Display(显示器/DPI 工具)、FilePreviewCommonThemes(深浅主题监听)、Telemetry/ManagedTelemetryCalculatorEngineCommonCommon.SearchCommon.UICommon.UI.Controls(WinUI 控件与转换器)、LanguageModelProvider(本地 AI/语言模型抽象,服务于 AdvancedPaste 等模块)、PowerToys.ModuleContracts(供新式托管模块遵循的 .NET 接口)。

理解这层目录的价值在于:无论你研究的是原生 C++ 模块(如 FancyZones)还是 C# 模块(如 PowerRename、Image Resizer),其依赖的底层能力都能在 src/common 找到对应实现,例如快捷键冲突检测、GPO 策略门控、主题联动、遥测事件这些跨模块横切关注点,全部沉淀于此。

六、源码阅读的辅助入口与结语

回到 src/README.md 的四行结论:模块 DLL + 宿主 Runner + 独立设置程序 + 共享静态库,这四者构成了 PowerToys 代码库的骨架。一旦掌握了这条主线,再看具体某个 PowerToy(例如 FancyZones 的窗口布局引擎、PowerRename 的托管宿主交互)时,就能清晰地把它放入整体架构中——它从哪里被加载(runner 的 knownModules 清单)、遵循什么契约(PowertoyModuleIface)、配置如何流转(get_config/set_config 的 JSON 序列化)、可复用什么底座(src/common)——从而大幅降低在数十万行代码中的定位成本。

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