PowerToys FancyZones 架构设计解读:从 WorkArea/ZoneSet 模型到多显示器多虚拟桌面的布局管理重构
导读
本文基于微软 PowerToys 仓库中的 FancyZones-DCR 设计文档(Design Change Request),系统梳理 FancyZones 窗口分区管理器的核心对象模型:Zone、ZoneSet、WorkArea 与顶层 FancyZones 类各自承担的职责,并结合当前仓库源码分析其真实落点。在此基础上,文章深入剖析该 DCR 的核心议题——当显示环境变化(切换虚拟桌面、增删显示器等)时反复触发 EnumDisplayMonitors 所导致的 WorkArea 对象频繁销毁重建问题,以及文档给出的"按虚拟桌面+显示器去重缓存、按需增量创建"改进方案。读完本文,你将能够基于这份设计文档理解 FancyZones 在桌面/窗口事件驱动下的整体工作流,掌握其从"全量重建"到"增量同步"的设计演进思路,并能在源码中定位每一层对象与对应实现文件。
一、文档背景:一份 FancyZones 的设计变更请求(DCR)
本文所依据的 FancyZones-DCR.md 位于仓库 doc/specs/ 目录下,该目录与 SCOOBE.md、settings-search.md、power-display-adaptive-brightness.md 等并列,存放 PowerToys 各类功能的设计文档。FancyZones 本体代码位于 src/modules/fancyzones,其核心算法库是 src/modules/fancyzones/FancyZonesLib。
文档结构十分清晰:前半部分是 FancyZones library 的四层对象架构简介(§1–§4 即 Zone/ZoneSet/WorkArea/FancyZones 四个类),后半部分则围绕第 4 类"环境变化处理"提出修改提案(Proposal)。因此,本文也按"现状架构 → 痛点分析 → 改进方案 → 源码印证"的脉络展开。
二、FancyZones 核心对象模型:四个类各司其职
1. Zone:矩形结构的轻量封装
文档原文将 Zone 定义为:
Class which is basically wrapper around rectangle structure, representing one zone inside applied zone layout.
即在已应用的布局中,一个 Zone 就代表一个"可吸附"的窗口区域,本质上是矩形(RECT)结构的封装,ZoneSet 以数组形式持有这些 Zone,共同构成一个完整布局。
源码佐证见 FancyZonesLib/Zone.h:class Zone 的私有成员正是 const RECT m_rect 与 const ZoneIndex m_index,并通过 GetZoneRect()、Id()、IsValid()、GetZoneArea() 等接口对外暴露矩形、序号、有效性与面积。ZoneIndex 与 ZoneIndexSet 也被定义在同一文件中(using ZoneIndex = int64_t; using ZoneIndexSet = std::vector<ZoneIndex>;),其中 ZoneIndexSet 表示一次窗口吸附所跨过的全部 zone 序号集合。此外 Zone.h 还定义了间距下限常量 ZoneConstants::MAX_NEGATIVE_SPACING = -20,说明布局的 zone 间距允许为负(区域交叠)但不超过 -20 像素。
2. ZoneSet(现已演化为 Layout):布局的几何计算器
文档原文将 ZoneSet 定义为:
Class implementing actual zone layout applied... responsible for actual calculation of rectangle coordinates (whether is grid or canvas layout) and moving window through them.
即 ZoneSet 负责两种布局模式下各 zone 矩形坐标的实际计算:
- Grid(网格)布局:由行列/单元格模板自动推导各 zone 的矩形;
- Canvas(画布)布局:每个 zone 由用户自由摆放的矩形定义。
以及在这些 zone 之间移动窗口。当前 WorkArea 持有的"当前生效布局"即为 ZoneSet。
需要说明的是,这份 DCR 写作年代较早,命名在后续版本中发生了演进:从当前源码看,负责持有 zone 集合与几何计算的核心类已更名为 Layout,见 FancyZonesLib/Layout.h。Layout 持有 ZonesMap m_zones,其接口 ZonesFromPoint()(命中测试)、GetCombinedZoneRange()(计算跨越起始/终止两个 ZoneIndexSet 的最小包围盒范围内的全部 zone)、GetCombinedZonesRect() 正对应文档描述的"矩形坐标计算与跨 zone 移动窗口"职责。因此,可以认为 当前源码中的 Layout 类继承了原 DCR 中 ZoneSet 的职责与定位。布局类型的枚举 ZoneSetLayoutType(包含 focus/grid/rows/columns/priority-grid/custom 等值)仍保留在 FancyZonesDataTypes.h 中,印证了类型层面与旧命名的延续关系。
3. WorkArea:由"显示器 × 虚拟桌面"确定的唯一工作区
文档给出了一个非常关键的定义:
Class representing work area, which is defined by monitor and current virtual desktop... if You have two monitors connected and two virtual desktops, You have 4 work areas available, and each of them can have separate zone layout.
即 WorkArea = 某一台显示器 × 某一个虚拟桌面 的组合。例如 2 台显示器 + 2 个虚拟桌面 = 4 个 WorkArea,每个 WorkArea 可以拥有彼此独立的分区布局,且持有当前生效的 ZoneSet(现在为 Layout)。
源码印证见 FancyZonesLib/WorkArea.h:WorkArea 以 FancyZonesDataTypes::WorkAreaId m_uniqueId 唯一标识自身,持有 std::unique_ptr<Layout> m_layout(即文档所说的 active ZoneSet)、LayoutAssignedWindows m_layoutWindows(窗口与布局的吸附绑定关系)、以及一个隐藏工具窗口 HWND m_window("Hidden tool window used to represent current monitor desktop work area",用于承载 overlay 交互)。其创建采用工厂方法 WorkArea::Create(hinstance, uniqueId, parentUniqueId, workAreaRect)——注意这里的 parentUniqueId 参数,它用于支持当前文档未覆盖、但后续版本引入的布局继承能力(新 WorkArea 可从"父 WorkArea"继承布局配置)。对外行为接口包括 Snap()/Unsnap()(吸附/解除)、ShowZones()/HideZones()/FlashZones()(overlay 显隐与闪烁提示)、CycleWindows()(在 zone 内循环切换窗口)等。
4. FancyZones:顶层入口与一切用户动作的调度者
文档将 FancyZones 定位为:
Top level entity and entry point for all user actions (which goes through actual module interface).
并归纳了四项主要职责:
- 按用户请求携带适当命令行参数启动 FancyZones Editor(C#):即设置界面中"编辑所选区域布局"时拉起编辑器进程。相关参数构造逻辑可在 FancyZonesLib/EditorParameters.h 与
.cpp中找到(例如传递 work area id、monitor 信息等),而 Editor 本体为 C#/WPF 项目,位于 src/modules/fancyzones 下的editor/及 FancyZonesEditor 相关工程。 - 跟踪每台显示器上当前激活的 WorkArea:即"显示器 → 当前活跃 WorkArea"的映射维护。
- 跟踪当前活跃的虚拟桌面:文档特别注明这是在独立线程中通过轮询
VirtualDesktopIDs注册表键并解析其内容完成的。对应源码见 FancyZonesLib/VirtualDesktop.cpp 与.h,其中实现了虚拟桌面变化监听/查询能力。 - 检测工作环境的一切变化,包括:创建 / 销毁 / 切换虚拟桌面、关闭 FancyZones Editor、显示设置变更(分辨率、显示器热插拔等),并处理这些变化。
从模块接口层面看,FancyZones 通过 COM 接口暴露自身,顶层接口定义见 FancyZonesLib/FancyZones.h:IFancyZones::Run()/Destroy() 负责启停;IFancyZonesCallback 则定义了运行期回调,其中 VirtualDesktopChanged()(虚拟桌面切换通知)与 HandleWinHookEvent()(窗口移动/缩放等 WinEventHook 事件)正是文档第 4 点所述"环境变化"驱动 FancyZones 内部逻辑的两大入口。该文件同时把事件回调到核心逻辑的桥接方式写得很清楚:FancyZones.cpp 中 VirtualDesktopChanged() 的实现(FancyZones.cpp)注明"该回调来自可重入的 WinHookProc 函数,因此必须把实际逻辑推迟处理"——这与本文下一章讨论的"在事件风暴下避免反复重建对象"是同一设计语境。
三、痛点分析:为什么反复调用 EnumDisplayMonitors 会成为问题
DCR 文档第 14 行起("Proposal for modifications of handling described in 4.4")剖析了既有实现的问题:
现状:面对上节第 4 点所述的任何环境变化(虚拟桌面增删切换、Editor 关闭、显示设置变更),FancyZones 都会调用 Win32 API EnumDisplayMonitors 并传入回调函数。EnumDisplayMonitors 是异步工作的,会为每个可用 WorkArea 触发一次回调——沿用前文的例子,2 台显示器 × 2 个虚拟桌面时回调被触发 4 次。
问题:每次回调触发时,代码都不加区分地为该 WorkArea 销毁旧的 WorkArea 对象并创建新的对象,即使实际上什么都没变(典型场景:用户在两个虚拟桌面之间来回切换)。文档原文的表述是:
This constant creation and deletion of WorkArea has caused some problems in the past and it's not ideal for some other fixes we would like to make in the multi-monitor/multi-desktop scenario.
即这种持续的创建/删除行为:
- 在过去已经引发过一些实际 bug;
- 且对后续想在"多显示器 + 多虚拟桌面"场景推进的其他修复/特性而言并非理想基础。
从工程角度不难推断其代价:每次全量重建都意味着重新走一遍 WorkArea 初始化(包括WorkArea.h 中 InitWindow() 创建隐藏工具窗口、InitLayout() 加载布局、恢复 LayoutAssignedWindows 吸附绑定等一系列重活),同时伴随窗口消息、overlay 状态与 JSON 持久化层的连带抖动,属于典型的"无差别全量重建"资源浪费。
四、改进方案:基于虚拟桌面跟踪器的增量同步
4.1 设计思想:把"每次全量重建"换成"按需增量创建"
文档提出的方案并不复杂,但切中要害:
- 复用既有虚拟桌面跟踪器(见文档 §4.3,即上文
FancyZones第 3 项职责中基于轮询VirtualDesktopIDs注册表键的实现),并将其能力稍作扩展,使系统能够判断某个 WorkArea 到底是"新出现的"还是"已经处理过的"; - 对于已经处理过的 WorkArea,跳过"删除旧对象 + 创建新对象"的流程,即使它的环境通知再次到达(例如虚拟桌面来回切换);
- 维护一张映射表:以虚拟桌面 id 为 key,值为该虚拟桌面上已经处理过的显示器集合。文档特别注明虚拟桌面是跨所有显示器存在的("virtual desktop exists across all monitors"),因此一张虚拟桌面下需要登记全部显示器;
- 当收到
EnumDisplayMonitors回调、携带着"虚拟桌面 id + 显示器"所定义的工作区时,先去查这张表判断其是否为新工作区:- 是新的 → 创建对应的
WorkArea对象; - 已存在 → 直接跳过(不重建)。
- 是新的 → 创建对应的
- 删除虚拟桌面这一动作(同样由虚拟桌面跟踪器登记)会触发该映射表的对应更新,同时更新 FancyZones 的 JSON 存储(即持久化层,如各显示器/桌面的布局分配记录)。
这套方案的本质是从"面向事件的盲目重建"转向"面向状态的增量同步"——把 EnumDisplayMonitors 的多次异步回调收敛为"与已有状态做差集"的去重操作。
4.2 源码中的呼应:状态表与 JSON 持久化
虽然 DCR 描绘的是目标态设计,当前仓库代码已经从多个侧面印证了该方向的实际落地:
- 虚拟桌面跟踪器:文档 4.3 所述基于
VirtualDesktopIDs注册表轮询的实现,对应 FancyZonesLib/VirtualDesktop.cpp。它向 FancyZones 提供虚拟桌面的创建/切换/删除等事件信号,正是"判断 WorkArea 新旧"所需的前提信息源。 - 工作区与布局绑定关系:
WorkAreaConfiguration(见 FancyZonesLib/WorkAreaConfiguration.h)负责按WorkAreaId关联/更新每个工作区的布局与窗口分配,是"已处理过的显示器集合 + 布局"这一类状态映射在代码层面的承载;WorkAreaId的类型定义(monitor + virtual desktop 的组合键)见 FancyZonesDataTypes.h。 - JSON 存储层:DCR 中"删除虚拟桌面时同步更新 JSON 存储"所指向的持久化,对应 FancyZonesLib/FancyZonesData 目录下的数据组件,例如
AppliedLayouts(AppliedLayouts.h /AppliedLayouts.cpp,记录"work area → 已应用布局"映射)与AppZoneHistory(AppZoneHistory.h /AppZoneHistory.cpp,记录"应用窗口 → 上次所在 zone"历史,供窗口恢复吸附位置使用)。这类数据以 JSON 落在用户的 PowerToys 配置目录下,从而保证布局与窗口分配在虚拟桌面/显示器增删、甚至应用重启后依然可恢复。 - 增量创建的另一佐证:前文提到 WorkArea.h 的
Create()接收parentUniqueId,当某工作区首次出现时可从"父工作区"继承布局。这一机制的成立前提,正是系统能够区分"新 WorkArea"与"既有 WorkArea"——与 DCR 提议的查表判断逻辑一脉相承。
整体上可以推断,DCR 提出的"虚拟桌面 id → 已处理显示器集合"的去重映射,与当前源码中"WorkAreaId(显示器 + 虚拟桌面)作为键、WorkAreaConfiguration/AppliedLayouts 作为状态与持久化容器"的架构在职责划分上是一致的:前者解决运行期对象生命周期的抖动,后者解决布局状态的归属与恢复。
五、设计要点的工程启示
从这份 DCR 中,可以提炼出几条对理解 FancyZones(乃至同类多桌面窗口管理工具)有价值的设计原则:
- 枚举 API 的语义与去重需求:
EnumDisplayMonitors这类"枚举 + 回调"API 天然会为当前所有组合产生通知,回调次数等于"显示器数 × 虚拟桌面数",且可能与真实变化无关。消费方必须自带"幂等/去重"逻辑,不能把"收到回调"等价于"状态真变了"。 - 对象生命周期与状态缓存分离:把昂贵的对象(WorkArea 及其隐藏窗口、overlay、布局绑定)生命周期控制在"真正变化时",而把"哪些组合已存在"这样的元状态单独用映射表缓存,能显著减少多显示器/多桌面场景下的重建风暴。
- 事件源与持久化的联动:虚拟桌面删除这类事件不仅要更新内存态(映射表、WorkArea 集合),还要同步更新 JSON 持久化(
AppliedLayouts等),否则会出现"内存已清理、配置残留"的不一致。 - 为未来功能预留增量基础:文档反复强调此改动"有利于后续多显示器/多虚拟桌面场景的其他修复",说明架构级的对象生命周期治理通常是后续功能(如跨桌面布局继承、窗口状态迁移)的前置条件——WorkArea.h 中
parentUniqueId所体现的布局继承即是这种前置价值的例证。
六、延伸阅读
- FancyZones 模块整体说明与入口:src/modules/fancyzones/README.md
- 核心对象定义:Zone.h、Layout.h、WorkArea.h、FancyZones.h
- 虚拟桌面跟踪与事件回调:VirtualDesktop.cpp、FancyZones.cpp
- 状态映射与 JSON 持久化:WorkAreaConfiguration.h、FancyZonesData/AppliedLayouts.cpp、FancyZonesData/AppZoneHistory.cpp
- 配套测试工程:FancyZonesTests 与 FancyZonesEditor.UnitTests(布局计算、WorkArea/ZoneSet 行为的单元测试所在地)
- 设计文档原址:doc/specs/FancyZones-DCR.md
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