ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WinUI 3 布局 RTL(从右到左)支持全解析:FlowDirection、UI 镜像与坐标系统设计

WinUI 3 布局 RTL(从右到左)支持全解析:FlowDirection、UI 镜像与坐标系统设计 WinUI 3 布局 RTL从右到左支持全解析FlowDirection、UI 镜像与坐标系统设计【免费下载链接】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 3microsoft-ui-xaml 仓库关于从右到左Right-to-LeftRTL界面支持的深度技术指南。文章以仓库内设计笔记 docs/design-notes/rtl.md 为骨架结合dxaml/xcp底层源码CUIElement、CMediaBase、CFontIcon、CTextBlock等的实测实现完整讲解 RTL 的触发机制、客户与 Xaml 各自的工作流程、坐标空间模型以及 Canvas 布局、Flow Direction 翻转、DComp 可视化与辅助功能等环节面临的挑战。读完本文你将理解 WinUI 3 中 RTL 是如何被定义、如何由宿主 HWND 触发、Xaml 内核如何通过翻转变换实现镜像以及为什么文本与图片不能镜像这件事如此复杂。1. 什么是 WinUI 3 的 RTL由 FlowDirection 定义RTL 的英文全称是Right-to-Left指界面内容自右向左排列的布局方式是阿拉伯语、希伯来语等从右往左书写语言的应用所必需的能力。在 WinUI 3 中一个窗口内容的 RTL 状态完全由顶层元素的FlowDirection决定而这个顶层元素就是XamlRoot.Content所引用的根元素。FlowDirection属性只有两个取值LeftToRight默认值与RightToLeft。设置该属性只影响该控件及其子元素内部的排列方式即UI 镜像UI Mirroring效果。文档中特别强调RTL 状态与语言完全无关它只由这一个属性定义FlowDirection是托管 Xaml 内容的唯一事实来源source of truth桌面应用与 IslandXaml 岛两种托管场景都遵循同一套规则。文档把 UI 镜像定义为窗口内容的镜像翻转并引用 Win32 平台的WS_EX_LAYOUTRTL作为底层机制——这正是下一节要讲的触发入口。2. 什么会触发 RTL 感知应用的 UI 镜像作用范围是什么镜像的触发责任在**宿主host**一方。宿主通过给 HWND 设置WS_EX_LAYOUTRTL扩展样式将窗口切到 RTL 模式这一操作会使该 HWND 上的 Xaml 内容以及 IXP Content Island切换到 RTL 模式同时XamlRoot.Content的FlowDirection变为RightToLeft镜像随之生效子 HWND 默认继承顶层 HWND 的 RTL 样式并遵循同样的逻辑流。对于 WinUI 3 桌面应用宿主就是使用 WinUI 3 框架的应用代码本身。这一变化是动态的——只要RTL样式被应用到 HWND 上Xaml 就会即时响应。2.1 客户工作流客户看到什么客户决定何时切换顶层 HWND 到 RTL。文档给出的判定代码判断当前线程 UI 语言是否为 RTL 语言LOCALE_IREADINGLAYOUT返回 1 表示 RTLbool IsRTLLocale() { LCID lcid MAKELCID(::GetThreadUILanguage(), SORT_DEFAULT); wchar_t localeName[LOCALE_NAME_MAX_LENGTH]; DWORD readingLayout 0; ::LCIDToLocaleName(lcid, localeName, LOCALE_NAME_MAX_LENGTH, 0); ::GetLocaleInfoEx(localeName, LOCALE_IREADINGLAYOUT | LOCALE_RETURN_NUMBER, reinterpret_castwchar_t*(readingLayout), sizeof(readingLayout) / sizeof(wchar_t)); // Value of 1 indicates RTL language. See MSDN for LOCALE_IREADINGLAYOUT. return readingLayout 1; }也可以用GetUserPreferredUILanguages等替代 API。文档在此给 Xaml 团队留了一条备注为了让用户得到正确的全球化体验应提供良好的示例和指南帮助用户构建全球化的应用。客户将WS_EX_LAYOUTRTL应用到顶层 HWND进入 RTL 模式LONG_PTR exStyle ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE); ::SetWindowLongPtr(hwnd, GWL_EXSTYLE, (exStyle | WS_EX_LAYOUTRTL));UI 镜像生效整个 HWND 的内容——包括标题栏按钮、客户区里的 Xaml 控件——都会按从右到左的方向绘制。需要注意镜像对图片images和图标icons没有任何影响它们不会被水平翻转。对客户而言判断当前 Xaml 内容是否处于 RTL 模式的依据就是XamlRoot.Content.FlowDirection。上图展示了 RTL 模式下界面整体镜像后的效果图片来自仓库 docs/images/RTL1.png。2.2 Xaml 工作流Xaml 看到什么、做什么客户在 HWND 上设置 RTL 样式的瞬间挂载在该 HWND 上的 Content Island 会把自己切换到 RTL 模式将自身的LayoutDirection属性设为 RTL触发StateChanged事件。文档在此留给 IXP 一个问题除LayoutDirection变化外还有哪些情形会触发StateChangedXaml 代码收到StateChanged事件后把XamlRoot.Content的FlowDirection更新为RightToLeft于是整个 UI 镜像到 RTL。关键分层ContentIsland.LayoutDirection是 Xaml 判断自己是否处于 RTL 世界的唯一事实来源而XamlRoot.Content.FlowDirection严格跟随它的值前者一变后者就跟着变。默认情况下顶层 HWND 的 RTL 样式会被子 HWND 继承同样流程随之重演。但如果子 HWND 显式设置了WS_EX_NOINHERITLAYOUT样式来阻断继承那么该子 HWND 及其 Xaml 内容、Content Island 就会继续以 LTR 模式工作——反之亦然。另外文档明确Xaml Island 的行为与桌面托管没有任何差别无论 Xaml 内容被托管在桌面窗口还是 Island 中这套机制完全一致。2.3 覆盖默认行为有些场景需要反默认例如宿主 HWND 处于 RTL但你希望某个 Xaml Island 保持 LTR。文档给出两条路线并强烈提醒覆盖只是特殊场景的兜底手段。设计注记绝大多数想要 RTL 的客户应该直接在 HWND 上使用 RTL 样式而不是用覆盖Override手段。因为 RTL 的 HWND 能保证操作系统正确对待它系统菜单布局会是 RTL、工具提示tooltips与辅助功能accessibility支持同样如此。用 LTR 应用去伪装成 RTL 应用容易出现不可预期的行为。Xaml 想覆盖布局Xaml 代码可以直接设置ContentIsland.LayoutDirectionOverride为目标布局它随后会改变ContentIsland.LayoutDirection并沿上文描述的链路更新XamlRoot.Content.FlowDirection。客户想覆盖布局典型例子是Xaml Island 托管在一个不应跟随宿主 HWND 行为的 HWND 上。但目前没有一个明确、无歧义的客户侧流程来做这件事——ContentIsland.LayoutDirectionOverride并不是公开 API。对于弹窗里的 Xaml Island 不想跟随顶层 HWND 布局方向这类需求可以退而求其次地使用上文提到的WS_EX_NOINHERITLAYOUT样式阻断继承。文档还记录了一个未解决的 Xaml 任务XamlRoot.Content.FlowDirection之所以不能用来覆盖默认的LayoutDirection是因为该属性没有未设置/默认状态——它的默认值就是LeftToRight。一旦用它来做覆盖判定一个 RTL HWND 用户只要把XamlRoot.Content换成新内容就会因为默认值是LeftToRight而意外地把LayoutDirection覆盖掉——Xaml 根本无法区分这个值是默认值还是用户显式设置用户升级到开启 RTL 支持的更新版 WinUI 时也会看到行为差异。因此文档提出需要引入FlowDirectionOverride——要么给FlowDirection增加一个新值要么提供一个独立的XamlRoot.Content专属属性。3. XamlWinUI 3中的坐标空间RTL 模式下会牵涉多套同时存在的坐标系统文档明确给出了 RTL 坐标模式的定义原点切换到右上角X 轴自右向左递增Y 轴方向不变自上而下。下面是各坐标系统的 RTL 行为对照坐标系统RTL 行为OS 坐标系统clientRTL HWND 会切到 RTL 模式屏幕坐标始终保持 LTR原点在左上但 OS 在需要 Screen → Client 转换时自动完成换算InputIXP坐标系统应当是 RTLPointerPoint也遵循该坐标。但当前是已知缺陷IXP taskIXP 输入坐标系统尚未切到 RTL导致 PointerPoint 坐标错误DComp 输出IXP坐标系统始终保持 LTR 模式——这正是图片、图标在 RTL 模式下也不会镜像的原因从 DComp visual 取点时需要做坐标转换。当前存在转换缺陷IXP taskDComp 坐标空间中的点被错误地换算到屏幕坐标引发大量 RTL ↔ LTR 相关的 bugXaml 坐标空间即 Xaml 控件内部使用的客户区坐标系统会同步切换到 RTL 模式4. Canvas 在 RTL 下的行为原点在右上X 越大越靠左在面向应用的公开 API 层面Xaml 的 RTL 行为是符合直觉的RTL 模式下原点(0, 0)在右上角X 值增大方向是向左。文档给出了一个可直接运行的示例标记Canvas Width800 Height600 BackgroundRed FlowDirectionRightToLeft Canvas Width100 Height100 BackgroundGreen Canvas.Left200 TextBlock FontSize24 Textasdf ForegroundWhite/TextBlock /Canvas /Canvas上图为上述标记的实际渲染结果来自仓库 docs/design-notes/images/rtl-sample-markup.png。需要记住的观察点红色 800 宽的Canvas被标记为 RTL绿色 100 宽的Canvas通过Canvas.Left200定位在其内部绿色 Canvas 里有一个TextBlock绿色Canvas渲染在靠近右上角的位置——那里就是原点(0, 0)Canvas.Left这个名字具有误导性但它确实把绿色Canvas移到了右边——其行为印证了增大 X 坐标就是向左移动由于 RTLTextBlock被对齐到其父绿色Canvas的右侧而文字本身仍然正确渲染没有被水平镜像。图中黄色标记点的坐标含义值得细品相对外层 CanvasX 200相对内层 CanvasX 0相对 TextBlockX 0——这一点很有意思因为它遵循的是 RTL 坐标系TextBlock自身仍是 RTL 元素尽管其中的字形glyphs是按 LTR 渲染的。文档还补充了一个结论Popup元素无论内联还是窗口化在 RTL 上与其他任何UIElement行为一致——它可以继承树上层的FlowDirection也可以拥有自己的FlowDirectionPopup下的元素可以像经过任何普通UIElement一样通过Popup继承 RTL。5. Flow Direction 变换Xaml 与 IXP 之间的翻转补偿当 Xaml 与 IXP 打交道时行为却完全不同即使在 RTL 模式下原点(0,0)仍在左上角X 增大方向向右。面向应用的公开 API 行为与面向 IXP 的内部记账行为不一致意味着 Xaml 必须写代码来补偿这个差异。5.1 根级翻转变换Xaml 会在其 RTL 子树的根部应用一个翻转变换flip transform。注意这个翻转可以发生在任意元素上——因为每个FrameworkElement都有FlowDirection属性。这个翻转是X 方向缩放为 -1X scale by -1外加一个平移translation否则内容会被缩放到负空间里去。两者合起来把 LTR 布局变成 RTL 布局。但仅仅做这个翻转还不够——如果只应用翻转得到的结果是文本被镜像上图展示了只做根级翻转、不做叶片级补偿时的错误结果来自仓库 docs/design-notes/images/rtl-sample-markup2.png。注意文本被水平镜像了。为此TextBlock和图片还需要一次就地翻转in-place flip把内容再正过来。关键在于这个就地翻转发生在叶leaf元素上所有布局变换都位于这次就地翻转的上方。5.2 叶片级翻转的两套机制机制一CDependencyObject::IsRightToLeftCUIElement::GetLocalTransform。CDependencyObject::IsRightToLeft方法指明某个对象是否应处于 RTL 状态。CUIElement::GetLocalTransform经由CUIElement::GetShouldFlipRTL在决定元素渲染所用的局部变换时会读取该值Xaml 据此自动按需添加就地翻转。源码佐证dxaml/xcp/core/core/elements/uielement.cppCUIElement::GetShouldFlipRTL的核心逻辑正是文档所描述的原则——只有当自身 RTL 状态与父级不一致时才会翻转先通过GetUIElementAdjustedParentInternal拿到父元素检查父元素与自身的IsRightToLeft()是否一致myRTLMatchesParentRTLflipRTL成立的条件是当前元素不是无父级 Popup且自身的 RTL 与父级不匹配或是 RTL 无父级 Popup 的子元素代码中还有针对 parentless Popup 的特殊处理无父级 Popup 本身不参与布局、不做翻转而是由它的子元素代为处理 RTL注释里还留着一个 TODO为什么无父级 Popup 自己不处理翻转、要由子元素来做。这与设计笔记在 Popup 一节Popup 像任何其他 UIElement 一样工作的表述相互印证。文档还列出了若干在源码中硬编码IsRightToLeft返回false的类型这些类型拒绝镜像CMediaBase::IsRightToLeftCImage继承自它——源码见 dxaml/xcp/core/core/elements/mediabase.cpp注释明确写着 Media objects do not inherit FlowDirection即媒体对象不继承流方向CGlyphs::IsRightToLeftCTextBoxView::IsRightToLeftCRun::IsRightToLeft——注意CRun是CDependencyObject而不是CUIElement这个重写不期望被调用断言为false。机制二CTextBlock::GetContentRenderTransform。CTextBlock通过CTextBlock::GetContentRenderTransform做自己的就地翻转。该方法在渲染期由CTextBlock::HWRenderContent调用相关实现分布在 dxaml/xcp/core/text/TextBlock/TextBlock.cpp 与TextBlockView.cpp中需要时应用就地翻转让文本保持 LTR 渲染。5.3 第三种机制CFontIcon 的可选 LTR 渲染CFontIcon还有第三种可选机制让图标始终以 LTR 渲染。它拥有MirroredWhenRightToLeft属性来指定图标是否需要镜像如果不需要镜像CFontIcon::ApplyScaleTransformForFlowDirection就会在 RTL 树中应用就地翻转。源码佐证dxaml/xcp/core/core/elements/icon.cppKnownPropertyIndex::FontIcon_MirroredWhenRightToLeft属性变化时会调用ApplyScaleTransformForFlowDirection()该方法读取该属性并把结果写到元素上。与前面两种机制不同的是CFontIcon没有直接插入渲染或命中测试管线而是把变换设置在元素的RenderTransform属性上——渲染和命中测试随后像对待任何其他 RenderTransform 一样拾取它。6. DComp 支撑的 VisualContent Island 与 IXP 的 RTL 边界Content Island 的 RTL 行为不会影响内容桥content bridge内部的渲染或输入——无论桥是 LTR 还是 RTLVisual 始终假定(0,0)在左上角。这对 Xaml 来说基本是好消息Xaml 可以继续做它已经在做的事——在内容桥内部按左上角原点假定做 RTL 翻转。但这引出一系列待决问题文档记录为 IXP taskIXP 需要评估这对 PopupWindowSiteBridges 的影响。该类有一个接收rect的MoveAndResizeAPI——如果 site bridge 处于 RTL 模式这个rect的(0,0)在哪里文档给出的参照是先例在 RTL OS 中::CreateWindow创建 hwnd 时(0,0)在何处相应的 Xaml task 是按需调整MoveAndResize调用。更进一步文档希望 IXP 侧也有与 Xaml 公开 API 一致的 RTL 行为会有只基于 IXP visuals 与 content bridges不含 Xaml构建的应用当桥处于 RTL 时它们希望(0,0)指内容桥的右上角。这给 Xaml 与 IXP 都带来挑战对 Xaml 最大的影响是Xaml 不能再假定(0,0)在左上角而必须读取 visual 所挂载内容桥的设置并相应调整——这意味着要撤销一部分之前做的镜像。注意这影响的不仅是 RTL 场景LTR 场景也一样如果内容桥标记为 RTL但应用声明了某个元素FlowDirectionLeftToRight那么需要被应用翻转变换的反而是这段 LTR 内容。还有就地翻转的归属问题文本与图片在 RTL 树中保持 LTR 渲染这属于 Xaml 的内部机制如果 IXP 侧的 RTL island 中叶片内容不该镜像IXP 也需要一个就地翻转能力Xaml 可以直接受益。文档中的一条内嵌判断很有价值Xaml 关心的其实不是某个元素是 RTL而是某个元素的 RTL 与其父级不同——每当出现这种情况Xaml 就在树中放一个翻转变换这正是上一节GetShouldFlipRTL源码所验证的!myRTLMatchesParentRTL逻辑。照此思路Xaml 只需更新其根 UIElement 来适应(0,0)有时在右侧这一事实树上其他所有元素都能保持原有行为。对应的 Xaml task这是需要为完整 RTL 支持做工作量评估的 Xaml 侧工作。此外还有一个大问题surface-backed 内容在 RTL 下如何处理。Xaml 的文本与图片正是这种内容。一个幼稚的 RTL 实现只是把翻转变换放在根部结果会把所有 surface-backed 内容例如文本见前一张镜像图片一并镜像——这看起来是不可取的行为但最终由 IXP 拍板。假如不可取Xaml 的变通做法就是在不希望被镜像的元素对 Xaml 而言是文本和图片上多做一次就地翻转。Xaml 之所以敢这么做是因为它知道文本与图片内容都处于树的最末端、下面没有子元素——但这个假设对 IXP并不安全一个由SurfaceBrush支撑的SpriteVisual仍然是一个ContainerVisual下面可以有子 Visual对这些 SpriteVisual 做就地翻转会顺带把其所有子级的 RTL 状态一并翻转。正确的做法是只对该 SpriteVisual 的 surface-backed 内容做翻转记作 IXP task。文档的排期建议这些 IXP 侧工作放到 1.2 wave 之后再做——改动量属于中等到显著级别File Explorer 或其它 Shell 场景并不需要它受益者是直接构建在 IXP API 上的客户而 1.2 wave 中 IXP API 按作者理解还不会公开。7. 辅助功能Accessibility与开放问题辅助功能工具使用的是屏幕空间坐标因此理论上在 RTL 下依然工作正常——但需要一个最终检查来确认工具上报的坐标是否正确包括指针坐标、Xaml 控件坐标等。文档为此登记了 Xaml task验证辅助功能工具能正常工作尤其是在涉及屏幕空间坐标以及输入/输出等任何局部坐标时。7.1 开放问题汇总文档以挑战与开放问题章节收尾这里汇总成表类别问题责任方IXP InputIXP 输入坐标系统未切换到 RTLPointerPoint坐标错误IXP taskIXP DCompDComp 坐标空间中的点被错误换算到屏幕坐标引发大量 RTL ↔ LTR bugIXP taskIXP Input除LayoutDirection外还有哪些情形触发StateChanged事件IXP 待答Xaml APIFlowDirection缺少未设置/默认状态无法承载 override 语义需新增FlowDirectionOverride值或独立属性Xaml taskIXP/XamlPopupWindowSiteBridges 的MoveAndResize在 RTL 模式下rect的(0,0)定位语义需按需调整调用IXP Xaml taskIXP Visual 树IXP 是否要尊重其 Visual 树中的 RTL 属性、surface-backed 内容的镜像问题、SpriteVisual子级被连带翻转的问题IXP task建议 1.2 wave 后Xaml 根元素Xaml 需更新根 UIElement 以适配(0,0)有时在右侧的情况Xaml task需评估工作量Accessibility验证辅助功能工具在屏幕空间坐标与局部坐标下的正确性Xaml task8. 延伸阅读完整设计笔记原文docs/design-notes/rtl.md翻转判定核心实现dxaml/xcp/core/core/elements/uielement.cppCUIElement::GetShouldFlipRTL媒体/图片不继承 FlowDirection 的实现dxaml/xcp/core/core/elements/mediabase.cppFontIcon 的MirroredWhenRightToLeft与就地翻转dxaml/xcp/core/core/elements/icon.cppTextBlock 渲染期的就地翻转dxaml/xcp/core/text/TextBlock/TextBlock.cpp仓库内其他全球化/布局相关设计笔记docs/design-notes/rtlEndToEnd.md、docs/design-notes/OneCoreTransforms.md【免费下载链接】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),仅供参考
返回列表