WinUI XAML 的 OneCoreTransforms 模式:visual-relative 坐标系与轻量化合成下的坐标空间设计
【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml
本文基于 WinUI(microsoft-ui-xaml)仓库中的设计文档 OneCoreTransforms.md,深入讲解 XAML 运行时中"OneCoreTransforms(OCT)模式"的来龙去脉:它为什么存在、visual-relative 坐标系如何成为 XAML 缩放体系"北极星"、桌面版与轻量化合成(lightweight compositing)版 Windows 之间坐标/缩放的差异矩阵,以及当前源码中 OCT 分支的真实形态。读完后你将能够定位 XAML 中所有与 OCT 相关的开关函数,理解XamlOneCoreTransforms::IsEnabled()桩化背后的工程决策,并掌握文档提出的"保留 OCT 路径、机会性收敛"的路线图。
什么是 OneCoreTransforms?
按设计文档的叙述(2020 年 4 月),在 RS4(Release 4)时期,XAML 团队开始为一套"新的标准窗口坐标空间"做铺垫。团队创建了一个名为OneCoreTransforms的模式:当该模式开启时,会告诉"开明"的组件(XAML 即是其中之一)在彼此之间改用visual-relative coordinates(视觉相对坐标)来工作。具体而言:
- 不再使用屏幕空间,不再调用
GetClientRect、ClientToScreen这类 Win32 函数换算坐标; - 坐标改由一个composition visual(合成视觉节点)及其相对该 visual 原点的一个 X、Y 偏移来定义。
当时的目标是让所有版本的 Windows 最终都使用这套 visual-relative 坐标空间,但由于优先级调整,该模式如今只在部分轻量化合成(lightweight compositing)SKU 上开启。
在 System XAML 中,OCT 模式下视觉树的挂载方式也不同:
| 运行环境 | 视觉树挂载方式 |
|---|---|
| System XAML + Desktop 版 CoreWindow | 基于 CoreWindow 的 HWND 取 composition target |
| System XAML + Lightweight Compositing CoreWindow | 从 CoreWindow 获取一个Composition Island,把 XAML 视觉树 parent 到该 Composition Island 上 |
设计文档指出,代码库中到处散落着大量if (OneCoreTransforms)开关(文档撰写时约 50 处)。在没有轻量化合成环境做测试的情况下持续演进 lifted XAML,这些代码路径很可能没有被正确维护,从而形成技术债。该文档的目的就是梳理这些差异,并讨论处理策略。
当前源码的印证:在今天的仓库中,OCT 已经完成了文档路线图之外的一个演进——它被整体"桩化"了。专门承载该模式的组件 XamlOneCoreTransforms 头部注释写得很直白:
// _ONECORETRANSFORMS_REMOVED_ // In the past we had a special mode called "OneCoreTransforms" to help us support // Windows 10x. We expect it to come back in some form when we support 10x gain. // Please see /design-notes/OneCoreTransforms.md for more information. class XamlOneCoreTransforms { public: enum class InitMode { Normal, ForceDisable }; static void EnsureInitialized(InitMode mode); static bool IsEnabled(); static void FailFastIfEnabled(); private: static bool s_enabled; static bool s_initialized; };而 XamlOneCoreTransforms.cpp 中的实现只剩下空壳:
void XamlOneCoreTransforms::EnsureInitialized(InitMode) { } bool XamlOneCoreTransforms::IsEnabled() { return false; // 恒为 false:OCT 永远关闭 } void XamlOneCoreTransforms::FailFastIfEnabled() { }也就是说,当前构建中 OCT 恒为关闭,所有调用点保留了"如果 OCT 开启应该怎么做"的分支逻辑(dead path),这与文档执行摘要中"该模式目前始终关闭,但未来支持类似版本的 Windows 时这段代码可以帮我们指路"的判断一致,且注释进一步说明团队预期它在支持 10x 相关场景时以某种形式回归。类设计本身也保留了工程上的"安全带":FailFastIfEnabled()在依赖 Win32 屏幕空间 API(如ClientToScreen、GetWindowRect)的调用入口做快速失败断言,防止 OCT 一旦重新启用后,这些路径在不知情下错误地运行。
术语:OCT、lightweight compositing 与 visual-relative coordinates 可以互换
设计文档明确:在该语境下,OneCoreTransforms(OCT)、lightweight compositing(轻量化合成)、visual-relative coordinates(视觉相对坐标)三个术语几乎总是可以互相替换——因为今天 OCT 模式只在基于轻量化合成的版本上启用。理解这一点后,读源码时看到这三个词出现的位置,指的都是同一套机制。
Visual-relative 坐标:XAML 缩放的"北极星"
XAML 的长期目标是尽可能多地使用 visual-relative 坐标,只在真正需要的那一刻才转换到其他坐标空间。在 XAML 框架内部,"visual-relative coordinates"一般指以 XAML 显示区域左上角为 (0,0) 的坐标空间,其刻度单位是所谓的 "logical" pixels(逻辑像素),通常等于当前显示器的显示缩放。
这一原则在源码中留下了清晰的痕迹。例如 CContentRoot::ShouldUseVisualRelativePixels 是 CContentRoot 对"是否使用 visual-relative 像素"的表态:
bool CContentRoot::ShouldUseVisualRelativePixels() { if (XamlOneCoreTransforms::IsEnabled()) { return true; } return false; }当前IsEnabled()恒为 false,因此桌面路径返回 false(屏幕/物理像素语义);但分支结构完整保留,OCT 回归时可自动切回 visual-relative 语义。
XAML 运行时内部的缩放:不一致的 Scale 应用方式
设计文档中最有信息密度的一张表,列出了 XAML 各种部署形态下"缩放是如何被应用的",以及 XAML 内部各关键 API 在每种形态下收到的是物理像素还是逻辑像素。这张表是理解 XAML DPI 类 bug 的核心参考,完整继承如下:
表:Scale 是不一致的(Scales are Inconsistent)
| 场景 | XAML 如何应用缩放 | PresentTarget 尺寸单位 | GetGlobalBounds 与 TransformToWorldSpace | CUIElement::TransformToRoot |
|---|---|---|---|---|
| System XAML Desktop CoreWindow | ScaleRenderTransform 设置在 XamlRootElement 上 | Physical(物理像素) | Physical | Physical |
| System XAML Lightweight Compositing CoreWindow | Scale 设置在 XAML 之上的 comp visual 上,XAML 继承缩放 | Logical(逻辑像素) | Logical | Logical |
| System XAML DesktopWindowXamlSource | Scale 设置在 XAML 之上的 comp visual 上,XAML 继承缩放 | n/a | Physical | Logical |
| Lifted XAML Desktop CoreWindow | Scale 设置在 XAML 之上的 comp visual 上,XAML 继承缩放;XamlRootElement 上设置了 XAML RasterizationScale | Physical | Physical | Logical |
| Lifted XAML Lightweight Compositing CoreWindow | 尚未设计(Not designed) | TBD | TBD | TBD |
| Lifted XAML DesktopWindowXamlSource | Scale 设置在 XAML 之上的 comp visual 上,XAML 继承缩放 | n/a | Physical | Logical |
从这张表可以读出两条重要信息:
- 同一个 API(如
TransformToRoot)在不同宿主形态下返回的像素语义不同(Physical vs Logical),这正是文档说的"这些差异使得编写正确代码变得困难"——任何跨形态复用的坐标换算代码都必须显式知道自己处于哪一行。 - Lifted XAML(即当前 WinUI 3+ 的 dxaml 架构)在 Lightweight Compositing 下的行为标注为"尚未设计/TBD",印证了执行摘要中"目前无法在 lightweight compositing 模式上测试 WinUI 3+"的说法。
源码印证:VisualTree 构造时根据 OCT 开关选择完全不同的根缩放策略,见 VisualTree.cpp:
if (m_coreContentRoot.GetType() == CContentRoot::Type::CoreWindow) { const auto config = XamlOneCoreTransforms::IsEnabled() ? RootScaleConfig::ParentApply : RootScaleConfig::ParentInvert; m_rootScale = std::make_shared<CoreWindowRootScale>(config, m_pCoreNoRef, this); }ParentApply(OCT:父级 visual 已携带缩放,XAML 直接继承)与ParentInvert(桌面:缩放要"反转"处理,由 XAML 自己用 ScaleRenderTransform/RasterizationScale 承担)两种模式,正是上表中"System XAML Desktop CoreWindow"行与"Lightweight Compositing CoreWindow"行差异的落地实现。
如何在 XAML 代码中定位 OCT 开关
设计文档给出了在 XAML 代码中查找 OCT 分支的四把"钥匙",这四处表示法都仍然存在于当前代码库中:
| 函数 | 含义 | 当前位置 |
|---|---|---|
XamlOneCoreTransforms::IsEnabled | 轻量化合成模式下为 true(当前恒为 false) | XamlOneCoreTransforms.h |
ShouldUseVisualRelativePixels | XAML 中 CContentRoot 的表示 | ContentRoot.cpp |
ShouldUseVisualPixels | XAML 中 CTextBoxBase 的表示 | TextBoxBase.cpp |
TxGetShouldUseVisualPixels | XAML 中又一种表示(TextServicesHost) | TextServicesHost.cpp |
后三者的实现都收敛到同一个源头,例如 CTextBoxBase::ShouldUseVisualPixels:
bool CTextBoxBase::ShouldUseVisualPixels() // TSF3 and UIA coordinates are based on visual relative pixel { // We should be calling contentRoot->ShouldUseVisualRelativePixels but there is a AppWindow on desktop bug for IME candidate window // For now, only apply visual pixels for TSF3 on WCOS. return XamlOneCoreTransforms::IsEnabled(); }注释透露了历史包袱:文本代码本应查询contentRoot->ShouldUseVisualRelativePixels(),但因为桌面 AppWindow 下 IME 候选窗口的一个 bug,暂且只对 WCOS 上的 TSF3 应用 visual 像素。文本子系统内部则通过 TextServicesHost::TxGetShouldUseVisualPixels 转发给 TextBox,形成第三层表示——这就是文档所说"多套表示法并存"的实据。
按文档的说法,在 XAML 测试基础设施中,部分代码会调用一些私有 API(如MapVisualRelativePoints)把坐标换算成屏幕位置以便注入输入;某些测试(特别是可访问性测试)在 OCT 模式下期望不同的坐标,还有部分测试在 OCT 开或关时直接退出。从当前源码结构看,MapVisualRelativePoints这类私有 API 调用点在当前公开仓库的测试代码中已检索不到,可以推断它随着测试基建的重写和 OCT 桩化而不再出现,但"OCT 开启时坐标期望值不同"这一测试设计约束,仍是将来启用 OCT 前必须回归验证的事项。
特殊子系统:UIA、DirectManipulation 与文本
UIA
轻量化合成模式下,XAML 通过一个特殊的私有 UIA 接口以 visual-relative 坐标通信(该接口未公开,文档标注需在内部确认后才能依赖)。文档路线图中给出的方案是:不要删除支持这些私有接口的代码,而是创建一个名字略不同的自有契约,使其与现有 UIA 接口 1:1 对齐,从而解除对私有接口的直接依赖;同时更新那些在 OCT 模式下期望不同坐标结果的自动化测试。
DirectManipulation
按代码注释,DirectManipulation 在 Desktop 版上以屏幕坐标工作,在 OCT/轻量化合成版上以visual-relative 坐标工作。这一差异在当前源码 InputServices.cpp 中仍完整保留:
if (XamlOneCoreTransforms::IsEnabled()) { // In XamlOneCoreTransforms mode we talk to DManip in visual-relative coordinates, no need // to apply the zoom scale. } else { const auto scale = RootScale::GetRasterizationScaleForElement(pDMContainer); // Note that this uniform scaling does not affect the determinant test above. inputTransform.Scale(scale, scale); } IFC(pDirectManipulationService->SetViewportInputTransform(pViewport, &inputTransform));即:OCT 下直接以 visual-relative 坐标与 DManip 通信、无需施加 zoom 缩放;桌面下则按元素的 RasterizationScale 缩放 viewport 输入变换。附录还提到,OCT 分支曾通过IDirectManipulationManagerPartner::EnableOneCoreTransforms让 DirectManipulation 进入 strict 模式(lifted DirectManipulation 预期默认即以 strict 模式运行)。从当前源码看,EnableOneCoreTransforms的调用点已随 OCT 移除而消失,"strict 模式"成为唯一路径——这正符合附录中"内部确认后即可删除相关代码"的预判。
文本缩放
文档对文本代码留了一个明确的 TODO:坐标空间处理仍需进一步调研——尤其是传给 TSF(Text Services Framework)的是哪些坐标、TSF 期望哪种坐标空间;如果解决了"Scaling Today"一节列出的坐标变换 API 差异(GetGlobalBounds、TransformToWorldSpace、TransformToRoot),文本代码可以大幅简化。从源码结构看,这条 TODO 并未完全落地:文本路径中仍有大量XamlOneCoreTransforms::IsEnabled()分支(如 TextBoxView.cpp、RichEditGripperChild.cpp、textrangeadapter.cpp、TextServicesHost.cpp 等),说明文本子系统的坐标统一仍是未完成的工作。
初始化入口:Normal 与 ForceDisable
OCT 的初始化有两个入口,对应文档"Other"分类中的两条记录:
- DXamlCore.cpp:
XamlOneCoreTransforms::EnsureInitialized(XamlOneCoreTransforms::InitMode::Normal)—— 正常初始化(CoreWindow 路径); - WindowsXamlManager_Partial.cpp:
XamlOneCoreTransforms::EnsureInitialized(XamlOneCoreTransforms::InitMode::ForceDisable)—— 对应文档附录"Force disable in win32 mode (WindowXamlManager_partial.cpp)",即 XAML Islands(win32 托管)场景下强制关闭 OCT。
当前EnsureInitialized是空实现,InitMode枚举作为 API 形态保留,保证将来 OCT 回归时调用方代码无需改动。
建议路线图:保留 OCT 路径,机会性收敛
文档对 WinUI 3.0 的提案是:保留 XAML 中的 "OneCoreTransforms" 代码路径,并在可能的时机机会性地向它们收敛——收敛时优先采用 OCT 路径而非非 OCT 路径。这会在发布版本中留下一些 dead code,理由是:3.0 会在尚无法测试 OCT 平台之前发布,但团队希望发布后尽快翻转开关支持这些平台;由于当前只能在 OCT OFF 的平台上测试,任何重构都无法保证足以支撑 OCT ON 的平台。
文档列出的具体计划(完整继承):
- 与 lifted 组件通信时,尽量使用 "client-logical" 坐标,为将来更多使用 visual-relative 坐标铺路。这需要内部协调;在 win32 上下文中运行时可能不可行。涉及:
- DirectManipulation
- TouchHitTesting
- 在合适场景机会性优先 OCT 代码路径。有些场景下 OCT 路径本身就是等价的 WinRT 函数,而旧代码的保留理由(XamlPresenter 支持、微妙的兼容性顾虑等)已不再那么重要。例如:OCT 下调用
CoreWindow.Bounds获取 CoreWindow 尺寸,桌面下调用GetClientRect;收敛方式是始终使用CoreWindow.Bounds。 - 机会性减少 XAML 对未收敛缩放函数的使用(对应"Scale 不一致"表)。XAML 运行时内部尽可能全部使用逻辑像素:
- 停止使用
GetGlobalBounds/TransformToWorldSpace,改用类似GetGlobalBoundsLogical的接口; - 停止使用
CUIElement::TransformToRoot; - 调研将 WindowPresentTarget 尺寸从物理像素改为逻辑像素;
- 回顾近期的 DPI 类 bug,找出其他有问题的函数。
- 停止使用
- UIA:移除对私有 UIA 接口的直接使用,创建改名的等价接口;
- 内部确认:在代码中保留一份私有接口副本(可能使用不同 GUID,暂不使用)是否可接受;
- 与其删除支持 UIA 接口的代码,不如创建一个名字略有差异、与当前 UIA 接口 1:1 对齐的自有契约;
- 更新那些在 OCT 模式下期望不同坐标结果的自动化测试。
- 测试代码:在探索使用公开 API 做输入注入的过程中,考虑保持 visual-relative 坐标空间更干净的方式。
其他轻量化合成考量:链接库
文档还指出一个容易被忽视的约束:XAML 当前链接的某些库在轻量化合成模式下不可用(例如user32.lib)。这意味着即使所有坐标逻辑都收敛完毕,OCT 回归前还需清理依赖面——任何仍在 OCT 开启路径上调 user32 的代码都会被FailFastIfEnabled()类断言暴露出来(当前代码库中这类断言注释明确写着// Due to ClientToScreen call、// Due to GetWindowRect等,见 DXamlCore.cpp 与 UIAHostEnvironmentInfo.cpp)。
附录:XAML 对 OCT 模式的使用清单(2020 年 4 月快照)
以下按文档附录的分类完整列出 XAML 使用 OCT 模式的位置清单,并标注其中在当前源码中仍能直接找到调用点的代表性位置(以dxaml/xcp/为前缀的仓库相对路径)。这份清单本身就是"约 50 个 OCT 开关"的技术债台账。
UIA
(未公开暴露;依赖此信息前请先内部确认。)
- 使用 visual-relative 的 UIA 接口。
Direct Manipulation
(lifted DirectManipulation 预期默认以 strict 模式运行;删除代码前请内部确认。)
- 告知 DirectManipulation 使用 strict 模式:
IDirectManipulationManagerPartner::EnableOneCoreTransforms - 告知 DirectManipulation 使用 strict 模式:
CInputServices::CreateViewportInteractionForRootVisual
TouchHitTesting
- TouchHitTesting 使用不同的坐标空间(既然已 lifted,该怎么办?)——当前代码中 TouchHitTestingHandler.cpp 仍以
bool isVisualRelative = XamlOneCoreTransforms::IsEnabled();分支处理。
坐标空间差异(缩放/偏移)
(此处有些东西可以拆解:哪些是因为其他组件在不同坐标空间工作造成的,哪些是因为 XAML 以不同方式安排树?对于 XAML 安排树不同的场景,OCT 与 islands 模式下不应当一致吗?)
- 缩放处理不同:
CJupiterControl::UpdateHdr - 转换为正确缩放:
ListViewBaseHeaderItemAutomationPeer_partial.cpp - 缩放处理不同(Text):
CTextRangeAdapter::GetBoundingRectangles(textrangeadapter.cpp) - 缩放处理不同(Text):
CRichEditGripperChild::UpdateCenterWorldCoordinate(RichEditGripperChild.cpp) - 缩放处理不同(Text):
CTextServicesHost::ShowGripper(TextServicesHost.cpp,其中applyRasterizationScale = !XamlOneCoreTransforms::IsEnabled()) - 缩放处理不同(Text):
CTextBoxView::TxGetViewportRect(TextBoxView.cpp) - 缩放处理不同(Text):
CTextBox::GetRectFromCharacterIndex(TextBox.cpp) - 缩放处理不同(Text):
HyperlinkAutomationPeer::GetBoundingRectangleCore - 缩放处理不同(UIA):
FrameworkElementAutomationPeer::GetBoundingRectangleCore(FrameworkElementAutomationPeer_partial.cpp) - 缩放处理不同(UIA):
CUIElement::GetClickablePointRasterizedClient(uielement.cpp) - 缩放处理不同、配置根缩放:
VisualTree::VisualTree(VisualTree.cpp) - 缩放处理不同:
CUIElement::GetRedirectionTransformsAndParentCompNode - 不获取桌面偏移:
AutoSuggestBox::GetAdjustedLayoutBounds(AutoSuggestBox_Partial.cpp) - 窗口化 popup 不施加偏移:
DXamlCore::GetTranslationFromTargetWindowToRootWindow - 缩放处理不同(Dmanip):
CInputServices::UpdateManipulationViewport(InputServices.cpp) - 缩放处理不同(软件键盘):
CInputPaneHandler::BringFocusedElementIntoView(BringIntoViewHandler.cpp)
Island 中应用缩放的方式不同
- 注册缩放变更通知(DXamlCore)
- 修复 OCT 的 scale/bounds 同步问题:
DXamlCore::UpdateScaleFactor(DXamlCore.cpp) - 在根元素上设置正确缩放:
HWWalk::RenderProperties(hwwalk.cpp,isRootAndOneCoreStrict)
挂接到 CoreWindow 的 comp island
- 获取/连接 CoreWindow 的 island:
CJupiterWindow::SetCoreWindow(JupiterWindow.cpp) - 获取/连接 CoreWindow 的 island:
DCompTreeHost(DCompTreeHost.cpp)
使用 WinRT API 代替 Win32 API
(看上去其中大部分都可以迁到 WinRT 选项,不是吗?可能是当时担心兼容性破坏,而 WinRT 版本其实是没问题的(如果它向下兼容到 RS4)。)
- 用 CoreWindow bounds 而非
GetClientRect取尺寸:CJupiterWindow::GetJupiterWindowPhysicalSize - 订阅
CoreWindow.Activated(桌面下使用WM_ACTIVATE):CJupiterWindow::RegisterCoreWindowEvents - HDR 使用 DisplayInformation:
CJupiterControl::UpdateHdr(JupiterControl.cpp) - 使用 WinRT PointerPoint 而非 Win32
POINTER_INFO:CInputServices::ProcessPointerExitedEventByPointerEnteredElementStateChange - 决定使用 WinRT DManip 命中测试:
CJupiterWindow::UseDirectManipulationHitTestEvent
软件键盘
- 软件键盘使用
FrameworkInputView而非 InputPane:CInputPaneInteractionHelper::RegisterInputPaneHandler(InputPaneInteractionHelper.cpp,仍以ShouldUseVisualRelativePixels()分支) - 以
useVisualRelativePixels=true初始化 CInputPaneHandler:InputPaneProcessor(InputPaneProcessor.cpp) - 禁用与 SIP 的遮挡检测:
ContentDialog::AdjustVisualStateForInputPan
其他
- 测试 hook:
ShrinkApplicationViewVisibleBounds - 用于检测 win32(xaml islands)托管:
SemanticZoom_partial.cpp(SemanticZoom_Partial.cpp) - 桌面版确保 OCT 关闭:
Page_partial.cpp - 不配置 CoreWindow bounds 行为:
DXamlCore - 判断是否全屏:
CMediaElement::InvokeImpl - 不同的 SetCapture/ReleaseCapture 方式:
PointerInputProcessor(PointerInputProcessor.cpp) - win32 模式下强制禁用:
WindowXamlManager_partial.cpp(即上文ForceDisable初始化入口) - 拖放:
XamlMobileDragOperation.cpp - 文本代码调用
CTextBoxBase::ShouldUseVisualPixels以不同方式处理缩放
总结:这篇设计文档对今天的价值
把文档与当前源码对照,可以勾勒出 OneCoreTransforms 的完整生命周期:
- 起源:RS4 时期为统一的 visual-relative 标准坐标空间而设计,最终只在 lightweight compositing SKU 上启用;
- 策略:WinUI 3.0 决定保留全部 OCT 分支、机会性向 OCT 路径收敛,宁可留下 dead code 也不赌不可测试的重构;
- 现状:OCT 已被整体桩化(
IsEnabled()恒为 false,_ONECORETRANSFORMS_REMOVED_标记),但约 30 个源文件中仍完整保留着"如果 OCT 开启"的分支逻辑,从根缩放配置(RootScaleConfig::ParentApply/ParentInvert)、DManip 坐标约定、文本/TSF 坐标空间,到 UIA 边界矩形换算; - 对开发者的实际意义:如果你在 XAML 运行时中调试 DPI/缩放/输入坐标类问题,
XamlOneCoreTransforms::IsEnabled()、ShouldUseVisualRelativePixels、ShouldUseVisualPixels、TxGetShouldUseVisualPixels这四个函数是所有坐标空间分叉点的总索引;FailFastIfEnabled()调用点则标出了所有"隐性依赖 Win32 屏幕空间"的位置——这两类标记共同构成了将来 OCT(或任何 visual-relative 化重构)回归时的最小回归面。
需要说明的适用前提:本仓库是 lifted XAML(dxaml)架构,对应 WinUI 3+;设计文档写于 2020 年 4 月,部分细节(如私有 UIA 接口、MapVisualRelativePoints测试基建、EnableOneCoreTransforms调用点)在当前公开代码中已检索不到,属于已演进/移除的部分,文中均已按"当前源码事实"与"文档历史快照"区分表述。
【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考