PowerToys FancyZones 开发指南:模块架构、配置数据流与调试测试实践
FancyZones 是 PowerToys 中的窗口管理工具,允许用户创建自定义布局来组织屏幕上的窗口。本文基于 PowerToys 仓库的 FancyZones 开发文档与配套源码,系统讲解 FancyZones 的项目分层、关键文件职责、配置文件的存储与同步机制、显示器检测与 DPI 处理,并给出可复现的调试案例、UI 测试体系与常见问题的排查方法,帮助开发者从源码层面完整掌握该模块的运行原理。
架构总览
FancyZones 由多个相互关联的组件构成。从源码结构看,模块位于 src/modules/fancyzones,目录划分为两个大类:
- src:包含 FancyZones 的源代码,进一步拆分为区编辑器(Editor)、区管理与窗口吸附(Runner)以及用户设置管理(Settings)三块逻辑。
- tests:包含 FancyZones 与编辑器的单元/集成测试及 UI 测试代码,仓库中对应
FancyZonesTests、FancyZones.UITests、FancyZonesEditor.UnitTests、FancyZonesEditor.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 |
各工程的代码结构关系如下图所示:
接口层:FancyZonesModuleInterface
该接口层暴露 FancyZones 与 Runner 之间的交互接口,负责通信与配置交换。它本身包含极少代码——从 FancyZonesModuleInterface 目录结构看,主体是一个轻量 DLL,真正的逻辑都下沉到 FancyZonesLib 等模块中实现。
UI 层:FancyZonesEditor 与 FancyZonesEditorCommon
- FancyZonesEditor:主 UI 实现,以
MainWindow.xaml为入口,当前仓库中还可看到画布/网格两种布局编辑方式的核心文件:MainWindow.xaml、CanvasEditor.xaml、GridEditor.xaml、LayoutPreview.xaml 以及 LayoutOverlayWindow.xaml,数据模型集中在Models/与ViewModels/子目录。 - FancyZonesEditorCommon:为编辑器提供数据结构与 I/O 辅助,其
Data/与Utils/两个子目录分别存放布局数据类与工具函数。
可以这样理解编辑器:它主要是一个可视化配置编辑器,另一项功能是给显示器应用布局。
后端实现:FancyZones 与 FancyZonesLib
- FancyZonesLib:核心逻辑实现,负责所有拖拽行为、拖拽过程中的布局 UI(由 C++ 代码生成的覆盖窗口)以及核心数据结构。
- FancyZones:FancyZonesLib 的封装层,负责启动与生命周期管理。
数据流
配置数据的流动遵循清晰的单向同步模型:
- 用户与 Editor 的交互结果被保存到 Settings 对应的 JSON 配置文件;
- Runner 读取这些 Settings,应用布局并管理窗口位置;
- 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),其返回的对象同时实现 IFancyZones 与 IFancyZonesCallback 两个 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.cs、LayoutData.cs、LayoutHotkeys.cs、LayoutTemplates.cs、Zone.cs、ZoneSet.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.cpp、JsonHelpers.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 集合,再逐层交给 MonitorViewModel 与 MonitorInfoModel 的构造器完成 UI 数据绑定。
区跟踪
窗口与区的归属跟踪同样在 FancyZones::MoveSizeUpdate 函数中实现:窗口被拖动到某个区时,函数记录窗口与区的对应关系,并维护"哪些窗口属于哪些区"的历史记录(对应 AppZoneHistory 数据文件)。
开发环境搭建与入门路径
前提条件
- Visual Studio 2026(或 2022 17.4+):用于构建与调试;
- Windows 10 SDK:确保安装最新版本;
- PowerToys 仓库:从代码托管平台克隆到本地。
搭建步骤
-
克隆仓库:
git clone https://gitcode.com/GitHub_Trending/po/PowerToys -
在 Visual Studio 中打开 PowerToys.slnx;
-
选择 Release 配置,构建解决方案;
-
若遇到构建错误,可尝试删除 x64 输出目录后重新构建。
三步熟悉 FancyZones
第一步:熟悉功能。 实际使用一遍 FancyZones 理解其行为,并通读官方产品文档中关于该工具的功能说明,形成"功能现象 → 代码行为"的对应心智模型。
第二步:能够构建和调试。 确保模块可以成功编译并调试。首次搭建时,可能需要通过 PowerToys 设置界面启动一次 Editor,以初始化配置文件——直接以工程方式运行 Editor 不会初始化这些文件(详见故障排查)。
第三步:通过修 Bug 学习。 查看现有的 Bug 与功能请求理解代码结构;用调试器跟踪特定功能的代码执行路径;研究 UI 测试代码,理解功能是如何被自动化验证的。
调试实践案例:从 UI 元素反查显示器分辨率数据源
开发文档记录了一个完整的调试案例:某用户反馈在多显示器、超宽屏环境下编辑器中显示器分辨率显示异常。排查思路是从 UI 元素反向追踪数据来源,过程可复现:
- 定位 UI 代码位置。由于问题在 Editor,先在
MainWindow.xaml中搜索 "monitor" 字符串,发现命中第 82 行与第 338 行;第 82 行属于模板定义,目标元素实际位于第 338 行附近的代码块。另一种更精确的做法是使用 AccessibilityInsights 检查该 UI 元素:目标元素本身没有 AutomationId,但其父节点 "List View" 有 AutomationId,复制该值回代码搜索即可精确定位到第 338 行。 - 追踪数据绑定。该
Text元素的文本绑定在MonitorItemTemplate中,元素名为ResolutionText,绑定到数据属性Dimensions。 - 沿属性反查。在 FancyZones 全部工程内搜索
Dimensions,发现其由ScreenBoundsWidth变量赋值,而该变量位于MonitorInfoModel构造器中;继续搜索发现MonitorInfoModel在MonitorViewModel构造器中实例化,显示器的宽高在此处被赋值。 - 定位初始化点。检查
App.Overlay.Monitors的所有引用,沿Monitors.Add调用链找到AddMonitor()方法——它只被ParseParams()调用,确认数据来源于editor-parameters.json。 - 确认写入端。
editor-parameters.json在 Editor 与 FancyZones 两个工程中都有写函数。进一步发现:当对EditorParameters调用save时,传入的参数是m_workAreaConfiguration,而该变量在UpdateWorkAreas内初始化,由此确认完整调用栈为UpdateWorkAreas() → IdentifyMonitors() → GetDisplays() → EnumDisplayDevicesW()。
该案例说明:排查 Editor 显示类问题时,应优先确认 editor-parameters.json 的写入时机与内容,因为编辑器的显示器信息完全依赖 Runner 侧 UpdateWorkAreas() 的枚举结果,而非编辑器自行检测。
调试环境设置与常见问题
调试设置
- 在 Visual Studio 中把 FancyZonesEditor 设为启动项目;
- 在需要的代码位置设置断点;
- 点击运行开始调试。
开发过程中可以随时用断点调试排查问题;也可以附加到正在运行的进程,在真实上下文中调试 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.
解决方法:通过 PowerToys 设置界面启动一次 FancyZones Editor。直接在项目内运行 Editor 不会初始化所需的配置文件,因此 JSON 文件为空或缺失时会触发反序列化异常。
已知问题
- 可能存在与 Editor 数据更新相关的未被发现的 Bug;
- 部分自动化测试在 CI 中通过,但在特定机器上失败;
- 不同显示器配置组合对测试的覆盖要求很高。
排查显示类反馈时,开发文档给出的经验是:先确认用户是否真的为该屏幕应用了布局(未应用布局时远端区域不显示区是正常现象),再判断是否可能是代码问题或游戏/应用自身不支持该分辨率的渲染问题;只有在用户正确使用功能后问题仍存在时,才深入代码排查。
UI 测试与测试策略
FancyZones 的 UI 测试基于 Windows Application Driver(WinAppDriver)实现。
运行测试前的准备
- 安装 Windows Application Driver v1.2.1;
- 在 Windows 设置中启用开发者模式(Developer Mode)。
运行步骤
- 如果 PowerToys 正在运行,先退出它;
- 从安装目录运行
WinAppDriver.exe;若已安装在默认目录(C:\Program Files (x86)\Windows Application Driver),可跳过此步骤,测试会自动拉起它; - 在 Visual Studio 中打开
PowerToys.slnx并构建解决方案; - 在测试资源管理器中运行测试(菜单
Test > Test Explorer或快捷键Ctrl+E, T)。
注意:显示在受测窗口之上的通知或其他应用窗口会干扰测试过程。
测试框架结构
所有 UI 测试用例都需要预配置的用户数据,并且必须在每个测试前重置这些数据。所需的用户数据文件即配置目录中的七个文件:EditorParameters、AppliedLayouts、CustomLayouts、DefaultLayouts、LayoutHotkeys、LayoutTemplates、AppZoneHistory。
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 文件为事实来源、事件驱动刷新的轻量方案。掌握 MoveSizeUpdate、UpdateWorkAreas 两条核心调用链,以及七个配置文件的读写路径,就具备了阅读和修改 FancyZones 大部分功能所需的全部地图;配合仓库中现成的 Editor 测试套件与 WinAppDriver 工具链,可以在本地完整复现文档所述的调试与测试流程。
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 StartedRust0624
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


