首页
/ PowerToys FancyZones 开发指南:模块架构、配置数据流与调试测试实践

PowerToys FancyZones 开发指南:模块架构、配置数据流与调试测试实践

2026-09-06 11:52:43作者:廉彬冶Miranda

FancyZones 是 PowerToys 中的窗口管理工具,允许用户创建自定义布局来组织屏幕上的窗口。本文基于 PowerToys 仓库的 FancyZones 开发文档与配套源码,系统讲解 FancyZones 的项目分层、关键文件职责、配置文件的存储与同步机制、显示器检测与 DPI 处理,并给出可复现的调试案例、UI 测试体系与常见问题的排查方法,帮助开发者从源码层面完整掌握该模块的运行原理。

架构总览

FancyZones 由多个相互关联的组件构成。从源码结构看,模块位于 src/modules/fancyzones,目录划分为两个大类:

  • src:包含 FancyZones 的源代码,进一步拆分为区编辑器(Editor)、区管理与窗口吸附(Runner)以及用户设置管理(Settings)三块逻辑。
  • tests:包含 FancyZones 与编辑器的单元/集成测试及 UI 测试代码,仓库中对应 FancyZonesTestsFancyZones.UITestsFancyZonesEditor.UnitTestsFancyZonesEditor.UITests 等工程。

整个模块划分为以下几个工程:

工程 职责 仓库位置
FancyZones 线程启动与模块初始化,是 FancyZonesLib 的 COM 包装层 src/modules/fancyzones/FancyZones
FancyZonesLib 核心后端逻辑,被 FancyZones 通过 COM 调用;内含 FancyZonesData 数据管理目录 src/modules/fancyzones/FancyZonesLib
FancyZonesEditor 布局创建与编辑的主 UI 实现 src/modules/fancyzones/editor/FancyZonesEditor
FancyZonesEditorCommon 存储编辑器数据并提供共享功能(数据结构与 I/O 辅助) src/modules/fancyzones/FancyZonesEditorCommon
FancyZonesModuleInterface FancyZones 与 PowerToys Runner 之间的接口层,代码量很少,大部分逻辑在其他模块 src/modules/fancyzones/FancyZonesModuleInterface

各工程的代码结构关系如下图所示:

FancyZones 编辑器代码结构图

FancyZonesEditorCommon 代码结构图

接口层:FancyZonesModuleInterface

该接口层暴露 FancyZones 与 Runner 之间的交互接口,负责通信与配置交换。它本身包含极少代码——从 FancyZonesModuleInterface 目录结构看,主体是一个轻量 DLL,真正的逻辑都下沉到 FancyZonesLib 等模块中实现。

UI 层:FancyZonesEditor 与 FancyZonesEditorCommon

  • FancyZonesEditor:主 UI 实现,以 MainWindow.xaml 为入口,当前仓库中还可看到画布/网格两种布局编辑方式的核心文件:MainWindow.xamlCanvasEditor.xamlGridEditor.xamlLayoutPreview.xaml 以及 LayoutOverlayWindow.xaml,数据模型集中在 Models/ViewModels/ 子目录。
  • FancyZonesEditorCommon:为编辑器提供数据结构与 I/O 辅助,其 Data/Utils/ 两个子目录分别存放布局数据类与工具函数。

可以这样理解编辑器:它主要是一个可视化配置编辑器,另一项功能是给显示器应用布局。

后端实现:FancyZones 与 FancyZonesLib

  • FancyZonesLib:核心逻辑实现,负责所有拖拽行为、拖拽过程中的布局 UI(由 C++ 代码生成的覆盖窗口)以及核心数据结构。
  • FancyZones:FancyZonesLib 的封装层,负责启动与生命周期管理。

数据流

配置数据的流动遵循清晰的单向同步模型:

  1. 用户与 Editor 的交互结果被保存到 Settings 对应的 JSON 配置文件;
  2. Runner 读取这些 Settings,应用布局并管理窗口位置;
  3. Editor 在更新配置后发送更新事件,FancyZones 收到事件后刷新内存中的数据。

即:Editor 启动时加载配置数据,FancyZones 启动时也会加载配置数据;Editor 更新配置后发出数据更新事件,FancyZones 接收事件后刷新当前内存数据——双方并不共享同一份内存,而是以磁盘 JSON 为唯一事实来源、以事件通知保持同步。

关键文件与源码组织

入口与生命周期:FancyZonesApp 类

FancyZonesApp.h 中定义的 FancyZonesApp 类负责初始化与管理 FancyZones 应用,持有 winrt::com_ptr<IFancyZones> m_app 成员,通过 COM 接口驱动后端库。其主要成员包括:

  • 构造函数:初始化 DPI 感知、设置事件钩子、创建 FancyZones 实例;
  • 析构函数:清理资源、销毁 FancyZones 实例、解除事件钩子;
  • Run 方法:启动 FancyZones 应用;
  • InitHooks 方法:设置 Windows 事件钩子以监控系统事件;
  • DisableModule 方法:向主线程投递退出消息;
  • HandleWinHookEvent / HandleKeyboardHookEvent 方法:处理 Windows 事件钩子回调。

对应的实现见 FancyZonesApp.cpp,其中通过 m_app.as<IFancyZonesCallback>() 取回回调接口,用于处理如 VirtualDesktopChanged() 等虚拟桌面切换事件。后端实例的创建入口是 FancyZones.cpp 中的 MakeFancyZones() 工厂函数(见 FancyZones.cpp#L1713),其返回的对象同时实现 IFancyZonesIFancyZonesCallback 两个 COM 接口。

数据管理文件(FancyZonesData)

布局与运行状态数据的管理集中在 FancyZonesLib/FancyZonesData 目录:

  • AppliedLayouts.h/cpp:管理不同显示器与虚拟桌面已应用的布局;
  • AppZoneHistory.h/cpp:跟踪应用窗口的区历史记录;
  • CustomLayouts.h/cpp:处理用户创建的布局;
  • DefaultLayouts.h/cpp:管理不同显示器配置下的默认布局;
  • LayoutHotkeys.h/cpp:管理布局切换热键;
  • LayoutTemplates.h/cpp:处理布局模板;
  • LastUsedVirtualDesktop.h/cpp:记录最近使用的虚拟桌面。

核心功能文件

以下文件同样位于 FancyZonesLib

  • FancyZonesDataTypes.h:定义 FancyZones 全局使用的数据类型;
  • FancyZonesWindowProcessing.h/cpp:处理窗口移动、调整大小等事件;
  • FancyZonesWindowProperties.h/cpp:管理窗口属性,如窗口被分配到哪个区;
  • JsonHelpers.h/cpp:JSON 序列化/反序列化工具;
  • Layout.h/cpp:定义布局区管理用的 Layout 类;
  • LayoutConfigurator.h/cpp:配置不同类型的布局(网格 grid、行 rows、列 columns);
  • Settings.h/cpp:管理 FancyZones 模块设置;
  • EditorParameters.h/cpp:编辑器参数(显示器工作区信息)的读写,它是 Editor 与 Runner 共享显示器信息的关键载体;
  • WorkArea.h/cpp 与 ZonesOverlay.h/cpp:工作区划分与拖拽时覆盖窗口的绘制。

编辑器 UI 与数据组件

编辑器的 XAML 组件包括主窗口 MainWindow.xaml/cs、画布编辑器 CanvasEditor、网格编辑器 GridEditor、布局预览 LayoutPreview 与布局覆盖窗口 LayoutOverlayWindow。数据组件方面,开发文档列出 EditorParameters.csLayoutData.csLayoutHotkeys.csLayoutTemplates.csZone.csZoneSet.cs 等数据类,它们位于 FancyZonesEditorCommon 工程中,与 C++ 端 FancyZonesData 目录的文件一一对应,共同维护同一套 JSON 配置。

配置管理:文件位置与同步机制

配置文件位置

所有 FancyZones 配置都保存在用户目录:

C:\Users\[username]\AppData\Local\Microsoft\PowerToys\FancyZones

目录下包含以下 JSON 文件:

文件 内容
EditorParameters 编辑器参数(显示器工作区信息)
AppliedLayouts 各显示器/虚拟桌面已应用的布局
CustomLayouts 用户创建的布局
DefaultLayouts 内置默认布局
LayoutHotkeys 布局切换热键
LayoutTemplates 布局模板
AppZoneHistory 应用窗口与区的历史记录

配置处理方式

FancyZones 没有集中的配置处理器,读写逻辑分散在两个工程:

  • Editor 侧:读/写处理器在 FancyZonesEditorCommon 工程中;
  • Runner 侧:读/写处理器在 FancyZonesLib 工程中(如 EditorParameters.cppJsonHelpers.cpp)。

数据同步方式为事件驱动:Editor 发送更新事件后,FancyZones 刷新内存数据。这解释了为什么"直接在项目内运行 Editor"会出问题——它绕过了正常的初始化路径,详见后文故障排查部分。

窗口管理:显示器检测、DPI 与区跟踪

显示器检测与 DPI 缩放

  • 显示器检测在 FancyZones::MoveSizeUpdate 函数中处理。该函数在 FancyZones.cpp#L494 中实现,其职责是转发到 WindowMouseSnap(拖拽窗口管理)与 m_draggingState 拖拽状态,窗口移动/缩放事件的钩子分发逻辑集中在该文件的 MoveSizeUpdate/MoveSizeEnd 系列方法(见 FancyZones.cpp#L967-L978 的事件处理分支)。
  • DPI 缩放方面:在无 DPI 缩放的场景下,FancyZones 只获取窗口位置,无需关心鼠标侧的 DPI 缩放信息;窗口缩放则通过系统接口完成,详细代码可看 WindowMouseSnap::MoveSizeEnd() 函数(实现位于 WindowDrag.cpp,其 MoveSizeUpdate 入口见 WindowDrag.cpp#L71)。

显示器分辨率数据的完整来源

通过一次对"编辑器中显示器分辨率显示不正确"问题的排查(详见下文调试案例),可以确认显示器信息的完整调用栈:

UpdateWorkAreas() → IdentifyMonitors() → GetDisplays() → EnumDisplayDevicesW()

UpdateWorkAreas() 定义于 FancyZones.cpp#L1161。它调用 IdentifyMonitors() 枚举显示器,再通过 GetDisplays() 最终落到系统 API EnumDisplayDevicesW(),把结果写入 EditorParameters(对应 m_workAreaConfiguration 变量)并落盘为 editor-parameters.json。编辑器启动时由 ParseParams() 读取该文件,经由 AddMonitor() 构建 App.Overlay.Monitors 集合,再逐层交给 MonitorViewModelMonitorInfoModel 的构造器完成 UI 数据绑定。

区跟踪

窗口与区的归属跟踪同样在 FancyZones::MoveSizeUpdate 函数中实现:窗口被拖动到某个区时,函数记录窗口与区的对应关系,并维护"哪些窗口属于哪些区"的历史记录(对应 AppZoneHistory 数据文件)。

开发环境搭建与入门路径

前提条件

  • Visual Studio 2026(或 2022 17.4+):用于构建与调试;
  • Windows 10 SDK:确保安装最新版本;
  • PowerToys 仓库:从代码托管平台克隆到本地。

搭建步骤

  1. 克隆仓库:

    git clone https://gitcode.com/GitHub_Trending/po/PowerToys
    
  2. 在 Visual Studio 中打开 PowerToys.slnx

  3. 选择 Release 配置,构建解决方案;

  4. 若遇到构建错误,可尝试删除 x64 输出目录后重新构建。

三步熟悉 FancyZones

第一步:熟悉功能。 实际使用一遍 FancyZones 理解其行为,并通读官方产品文档中关于该工具的功能说明,形成"功能现象 → 代码行为"的对应心智模型。

第二步:能够构建和调试。 确保模块可以成功编译并调试。首次搭建时,可能需要通过 PowerToys 设置界面启动一次 Editor,以初始化配置文件——直接以工程方式运行 Editor 不会初始化这些文件(详见故障排查)。

第三步:通过修 Bug 学习。 查看现有的 Bug 与功能请求理解代码结构;用调试器跟踪特定功能的代码执行路径;研究 UI 测试代码,理解功能是如何被自动化验证的。

调试实践案例:从 UI 元素反查显示器分辨率数据源

开发文档记录了一个完整的调试案例:某用户反馈在多显示器、超宽屏环境下编辑器中显示器分辨率显示异常。排查思路是从 UI 元素反向追踪数据来源,过程可复现:

  1. 定位 UI 代码位置。由于问题在 Editor,先在 MainWindow.xaml 中搜索 "monitor" 字符串,发现命中第 82 行与第 338 行;第 82 行属于模板定义,目标元素实际位于第 338 行附近的代码块。另一种更精确的做法是使用 AccessibilityInsights 检查该 UI 元素:目标元素本身没有 AutomationId,但其父节点 "List View" 有 AutomationId,复制该值回代码搜索即可精确定位到第 338 行。
  2. 追踪数据绑定。该 Text 元素的文本绑定在 MonitorItemTemplate 中,元素名为 ResolutionText,绑定到数据属性 Dimensions
  3. 沿属性反查。在 FancyZones 全部工程内搜索 Dimensions,发现其由 ScreenBoundsWidth 变量赋值,而该变量位于 MonitorInfoModel 构造器中;继续搜索发现 MonitorInfoModelMonitorViewModel 构造器中实例化,显示器的宽高在此处被赋值。
  4. 定位初始化点。检查 App.Overlay.Monitors 的所有引用,沿 Monitors.Add 调用链找到 AddMonitor() 方法——它只被 ParseParams() 调用,确认数据来源于 editor-parameters.json
  5. 确认写入端editor-parameters.json 在 Editor 与 FancyZones 两个工程中都有写函数。进一步发现:当对 EditorParameters 调用 save 时,传入的参数是 m_workAreaConfiguration,而该变量在 UpdateWorkAreas 内初始化,由此确认完整调用栈为 UpdateWorkAreas() → IdentifyMonitors() → GetDisplays() → EnumDisplayDevicesW()

该案例说明:排查 Editor 显示类问题时,应优先确认 editor-parameters.json 的写入时机与内容,因为编辑器的显示器信息完全依赖 Runner 侧 UpdateWorkAreas() 的枚举结果,而非编辑器自行检测。

调试环境设置与常见问题

调试设置

  1. 在 Visual Studio 中把 FancyZonesEditor 设为启动项目;
  2. 在需要的代码位置设置断点;
  3. 点击运行开始调试。

开发过程中可以随时用断点调试排查问题;也可以附加到正在运行的进程,在真实上下文中调试 Runner 侧模块。

常见问题

  • 首次运行出现 JSON 错误:先通过 PowerToys 设置 UI 启动一次 FancyZones Editor,初始化必要的配置文件(直接运行项目内 Editor 不会初始化配置文件)。
  • UI 相关疑难:使用 AccessibilityInsights 等工具检查元素属性,定位 AutomationId/ClassName。

部署与发布流程

部署

  • 本地测试:在 Visual Studio 中构建解决方案后,从输出目录直接运行 PowerToys.exe 即可完整体验模块行为。
  • 打包:使用 MSIX 打包工具制作安装包,确保所有依赖被包含(对应仓库 installer 目录下的 WIX 工程)。

发布

  • 版本号:发布遵循语义化版本(semantic versioning)。
  • 发布说明:记录所有变更、修复与新功能。
  • 发布操作:创建新 Release 并上传安装包与发布说明。

故障排查

首次运行 JSON 错误

错误现象:首次运行 Editor 时出现如下报错:

The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0.

FancyZones 首次运行 JSON 错误与解决入口

解决方法:通过 PowerToys 设置界面启动一次 FancyZones Editor。直接在项目内运行 Editor 不会初始化所需的配置文件,因此 JSON 文件为空或缺失时会触发反序列化异常。

已知问题

  • 可能存在与 Editor 数据更新相关的未被发现的 Bug;
  • 部分自动化测试在 CI 中通过,但在特定机器上失败;
  • 不同显示器配置组合对测试的覆盖要求很高。

排查显示类反馈时,开发文档给出的经验是:先确认用户是否真的为该屏幕应用了布局(未应用布局时远端区域不显示区是正常现象),再判断是否可能是代码问题或游戏/应用自身不支持该分辨率的渲染问题;只有在用户正确使用功能后问题仍存在时,才深入代码排查。

UI 测试与测试策略

FancyZones 的 UI 测试基于 Windows Application Driver(WinAppDriver)实现。

运行测试前的准备

  • 安装 Windows Application Driver v1.2.1;
  • 在 Windows 设置中启用开发者模式(Developer Mode)。

运行步骤

  1. 如果 PowerToys 正在运行,先退出它;
  2. 从安装目录运行 WinAppDriver.exe;若已安装在默认目录(C:\Program Files (x86)\Windows Application Driver),可跳过此步骤,测试会自动拉起它;
  3. 在 Visual Studio 中打开 PowerToys.slnx 并构建解决方案;
  4. 在测试资源管理器中运行测试(菜单 Test > Test Explorer 或快捷键 Ctrl+E, T)。

注意:显示在受测窗口之上的通知或其他应用窗口会干扰测试过程。

测试框架结构

所有 UI 测试用例都需要预配置的用户数据,并且必须在每个测试前重置这些数据。所需的用户数据文件即配置目录中的七个文件:EditorParametersAppliedLayoutsCustomLayoutsDefaultLayoutsLayoutHotkeysLayoutTemplatesAppZoneHistory

Editor 测试套件(对应仓库 FancyZonesEditor.UITests 工程):

  • ApplyLayoutTest.cs:验证按显示器应用与选择布局;测试显示器切换场景下的文件更新与行为;验证虚拟桌面变化场景;
  • CopyLayoutTests.cs:测试复制各类布局,验证 UI 与文件正确性;
  • CreateLayoutTests.cs:测试布局创建与取消操作,重点验证文件正确性;
  • CustomLayoutsTests.cs:测试用户创建布局的操作,覆盖重命名、高亮行变更、区数量变更;
  • DefaultLayoutsTest.cs:验证默认布局与用户布局文件;
  • DeleteLayoutTests.cs:测试各类布局的删除,同时检查 UI 与文件更新;
  • EditLayoutTests.cs:测试区操作:添加/删除/移动/重置/拆分/合并;
  • FirstLaunchTest.cs:验证 Editor 首次运行能正确启动;
  • LayoutHotkeysTests.cs:测试热键配置文件的正确性(注意:热键的实际行为在后端 FancyZones 中测试);
  • TemplateLayoutsTests.cs:测试内置布局的操作,覆盖重命名、高亮变更、区数量变更。

FancyZones 后端测试

  • LayoutApplyHotKeyTests.cs:聚焦热键相关功能,测试热键行为的实际实现。

测试策略

  • 单元测试:构建单元测试工程后,用 Visual Studio 测试资源管理器运行(Ctrl+E, T);
  • 集成测试:确保 FancyZones 模块整体按预期工作,覆盖不同的窗口布局与吸附行为。

辅助测试工具

编写测试时,可能需要查看元素的辅助功能(accessibility)数据以找到要点击的按钮,可用 AccessibilityInsights 或 WinAppDriver UI Recorder 完成。

注意:运行测试时请关闭辅助工具,重叠的窗口会影响测试结果。

关键问题答疑

Q:布局是如何存储和加载的?有集中的配置处理器吗? 没有集中的配置处理器。Editor 的配置读写在 FancyZonesEditorCommon 工程中,FancyZones C++ 工程的配置读写在 FancyZonesLib 工程中,双方读写的是同一组文件(位于 C:\Users\[用户名]\AppData\Local\Microsoft\PowerToys\FancyZones)。Editor 本质是可视化配置编辑器,附加功能是为显示器应用布局。Editor 启动时加载配置数据,FancyZones 启动时也会加载;Editor 更新配置后发送数据更新事件,FancyZones 收到事件后刷新内存数据。

Q:显示器检测与 DPI 缩放在哪些代码里? 显示器检测看 FancyZones::MoveSizeUpdate 函数;无 DPI 缩放场景下 FancyZones 只获取窗口位置,不需要鼠标 DPI 缩放信息;窗口缩放走系统接口,详见 WindowMouseSnap::MoveSizeEnd() 函数。

Q:FancyZones 如何跟踪哪些窗口属于哪些区? 同样在 FancyZones::MoveSizeUpdate 函数中实现,函数内维护窗口与区归属的历史记录。

Q:管理员工具的窗口能否被移动? FancyZones 在自身不以管理员身份运行时无法移动管理员级窗口;默认情况下,如果 PowerToys 以管理员身份运行,所有工具也都以管理员身份运行,此时该限制不生效。

结语

FancyZones 模块以"COM 接口层 + C++ 核心库 + WPF 编辑器"三层结构解耦了窗口吸附的后端逻辑与布局编辑的前端体验,配置持久化采用无集中处理器、以 JSON 文件为事实来源、事件驱动刷新的轻量方案。掌握 MoveSizeUpdateUpdateWorkAreas 两条核心调用链,以及七个配置文件的读写路径,就具备了阅读和修改 FancyZones 大部分功能所需的全部地图;配合仓库中现成的 Editor 测试套件与 WinAppDriver 工具链,可以在本地完整复现文档所述的调试与测试流程。

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