WinUI 布局 API 更新详解:IndexBasedLayoutOrientation 与 VisibleRect 的机制、实现与实战
2026/9/17 3:24:00 网站建设 项目流程

WinUI 布局 API 更新详解:IndexBasedLayoutOrientation 与 VisibleRect 的机制、实现与实战

【免费下载链接】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 仓库中的布局设计文档 layout-updates-for-lfl.md 展开,系统讲解Microsoft.UI.Xaml.Controls命名空间下为LinedFlowLayout等流式布局引入的三组核心 API:IndexBasedLayoutOrientation枚举、Layout.IndexBasedLayoutOrientation属性与Layout.SetIndexBasedLayoutOrientation受保护方法,以及VirtualizingLayoutContext.VisibleRect属性与VisibleRectCore虚方法。读完本文,你将理解这些 API 如何让ItemsView实现基于索引的键盘导航、如何让虚拟化布局感知可见视口并精确控制元素加载,并能仿照仓库源码在自定义布局中正确使用它们。

背景:Layout 抽象与布局 API 演进的动因

在 XAML 布局体系中,实现布局的一种标准方式是继承Layout基类(Layout.h),例如StackLayoutFlowLayoutUniformGridLayout等均是其子类。当前Layout主要被 ItemsRepeater 以及紧随其后的ItemsView控件所消费。典型用法如下:

<ItemsRepeater ItemsSource="{x:Bind People}" ItemTemplate="{StaticResource PersonTemplate}"> <ItemsRepeater.Layout> <StackLayout Spacing="20" /> </ItemsRepeater.Layout> </ItemsRepeater>

然而,仅凭"把元素排列出来"这一能力,消费方控件无法回答两个关键问题:

  1. 索引与空间位置的关系:给定索引N,索引N+1的项在它的右边还是下边?这直接决定了ItemsView能否用方向键实现符合直觉的键盘导航。
  2. 当前可见区域在哪:一个虚拟化布局要显示 1 万个图片项,不可能创建 1 万个容器,它必须知道"用户当前看到的视口"才能只实现该区域内的元素。

正是为了解决这两个问题,本设计文档新增了三组 API:

  • IndexBasedLayoutOrientation枚举 +Layout.IndexBasedLayoutOrientation属性 +Layout.SetIndexBasedLayoutOrientation方法;
  • VirtualizingLayoutContext.VisibleRect属性 +VirtualizingLayoutContext.VisibleRectCore虚方法。

这些 API 的出发点并非Layout自身的绘制需求,而是为消费LayoutItemsView等控件提供能力支撑,本文后续将逐一剖析。

IndexBasedLayoutOrientation 枚举:索引与布局方向的关联约定

枚举IndexBasedLayoutOrientation定义了一组常量,用于说明项索引与其布局方式之间是否存在相关性。它位于命名空间Microsoft.UI.Xaml.Controls中,取值如下:

|名称||说明| |-|-|-| | None | 0 | 项的布局与索引编号之间没有任何关联。 | | TopToBottom | 1 | 项按索引递增方向从上到下垂直排列。 | | LeftToRight | 2 | 项按索引递增方向从左到右水平排列。 |

在仓库的 IDL 定义(Microsoft.UI.Xaml.Controls.idl)中,该枚举归属于XamlContract第 5 版契约,与之配套的 API 也都在同一契约版本下引入。

Layout.IndexBasedLayoutOrientation 属性:只读地查询布局方向

Layout.IndexBasedLayoutOrientation属性用于获取项在数据源集合中按其索引排列的方向;如果索引与布局方向无关联,则返回None。其默认值为IndexBasedLayoutOrientation.None——设计文档的备注指出,这是因为VirtualizingLayoutNonVirtualizingLayout两个基类都把默认值指定为None

C# 签名:

public IndexBasedLayoutOrientation IndexBasedLayoutOrientation { get; }

从源码实现看,Layout.h 中维护了一个私有字段m_indexBasedLayoutOrientation,其初始化值就是winrt::IndexBasedLayoutOrientation::None;Layout.cpp 中的 getter 直接返回该字段(调试状态下可被LayoutsTestHooks强制覆盖,用于测试目的)。

属性的作用对象是消费方控件,而非 Layout 本身

这是一个容易误解的 API,值得单独强调:IndexBasedLayoutOrientation属性对Layout自身的排列行为没有任何影响。它是一份"元数据",供消费Layout的控件读取并据此调整自己的行为,最典型的消费者就是ItemsView控件。

ItemsView在内部实现TryGetItemIndex方法与内置键盘导航处理时都会用到该属性:

  • TryGetItemIndex:见 ItemsView.cpp,它先通过GetLayoutIndexBasedLayoutOrientation()取得布局的方向,进而推导出"水平距离优先"还是"垂直距离优先"(isHorizontalDistanceFavored = TopToBottomisVerticalDistanceFavored = LeftToRight),再以此决定视口内距离最近项的选择策略。
  • 键盘导航:见 ItemsViewInteractions.cpp,方向键行为会根据IndexBasedLayoutOrientation决定是偏向水平距离还是垂直距离;None时则完全按物理位置导航。

不同取值下的方向键行为对照

LinedFlowLayout返回LeftToRight为例:

  • 右箭头键:导航到下一个索引(从索引II+1);
  • 左箭头键:导航到上一个索引(从索引II-1);
  • 左右方向键基于索引移动,而上、下方向键则基于物理位置移动(例如跳到上一行/下一行同一物理位置的项)。

IndexBasedLayoutOrientation返回TopToBottom时行为恰好相反(上下键基于索引、左右键基于物理位置);当返回None时,四个方向键全部基于物理位置移动,与索引无关。

下表用图示(来自 docs/design-notes/images 目录)直观展示三种取值对应的布局形态:

|IndexBasedLayoutOrientation|说明|示意图| |-|-|-| | None | 项的布局与索引编号之间没有关联 || | TopToBottom | 项按索引递增从上到下排列 || | LeftToRight | 项按索引递增从左到右排列 ||

Layout.SetIndexBasedLayoutOrientation 受保护方法:自定义布局如何声明方向

SetIndexBasedLayoutOrientationLayout的受保护方法,供自定义布局(通常是Layout的子类)在内部调用,以设置IndexBasedLayoutOrientation属性的值。C# 签名与文档示例:

public class MyHorizontalLayout : NonVirtualizingLayout { public MyHorizontalLayout() { SetIndexBasedLayoutOrientation(IndexBasedLayoutOrientation.LeftToRight); Debug.Assert(this.IndexBasedLayoutOrientation == IndexBasedLayoutOrientation.LeftToRight); } }

对应底层实现见 Layout.cpp,该方法只是将传入的枚举值赋给m_indexBasedLayoutOrientation字段;而IndexBasedLayoutOrientation()getter 则负责对外返回该值。两者的声明位置可见 Layout.h。

仓库中的真实调用:LinedFlowLayout 的实践

LinedFlowLayout(LinedFlowLayout.h)是流式换行布局,它在构造函数中声明自己的方向:

// controls/dev/Repeater/LinedFlowLayout.cpp, LinedFlowLayout::LinedFlowLayout() SetIndexBasedLayoutOrientation(winrt::IndexBasedLayoutOrientation::LeftToRight);

完整上下文见 LinedFlowLayout.cpp。由于LinedFlowLayout的项随索引递增而逐行从左到右排布,声明LeftToRight后,ItemsView便知道左右方向键应基于索引跳转、上下方向键应基于物理行位置跳转——这正是文档中"LinedFlowLayout返回LeftToRight,因此右箭头键从索引I移到I+1"的源码级印证。与之配套的测试见 LinedFlowLayoutTests.cs。

VirtualizingLayoutContext.VisibleRect 属性:向布局暴露可见视口

背景:什么是虚拟化布局

VirtualizingLayout是不要求创建全部元素的布局。当用它展示 1 万个图片项时,它不必创建 1 万个项容器,而是按需实现、回收,从而在性能上远超非虚拟化布局。要实现这一点,布局必须回答"现在视口里能看到什么"——这正是VisibleRect的用武之地。

属性定义

VirtualizingLayoutContext.VisibleRect获取与Layout关联的FrameworkElement内的可见视口矩形:

public Windows.Foundation.Rect VisibleRect { get; }

数据来源与实现细节

设计文档的 spec note 明确指出:该可见视口来源于承载LayoutFrameworkElement所引发的FrameworkElement.EffectiveViewportChanged事件,即EffectiveViewportChangedEventArgs.EffectiveViewport。布局的protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)实现可以消费这个Rect来决定实现哪些项。

VirtualizingLayoutContext中,VisibleRect与既有属性RealizationRect命名风格一致(后者同样是Windows.Foundation.Rect类型,对应"实现窗口")。从源码看,VirtualizingLayoutContext.cpp 中VisibleRect()直接调用可被重写的VisibleRectCore();而针对ItemsRepeater的实际上下文实现 RepeaterLayoutContext.cpp 中,VisibleRectCore()返回的是ItemsRepeaterVisibleWindow()(可见窗口),RealizationRectCore()则返回RealizationWindow()(实现窗口)。可见窗口即由EffectiveViewportChanged机制维护,是布局判断"用户看得到什么"的依据。

LinedFlowLayout 如何利用可见视口冻结视线内的行

LinedFlowLayout大量使用context.VisibleRect()来决定布局行为。例如:

  • context.VisibleRect().Height作为滚动视口高度、用context.VisibleRect().Y作为滚动偏移来计算需要布局的行(见 LinedFlowLayout.cpp);
  • 依据可见视口高度结合缓存长度计算需要实现的元素区域(如 LinedFlowLayout.cpp 中(minimumCacheLength + 1.0f) * context.VisibleRect().Height);
  • 行定位、锚定等逻辑中同样依赖VisibleRect进行边界判定。

设计文档特别解释了为何VisibleRect是必要的:LinedFlowLayout需要"冻结"用户视线中那些行的布局,如果不用这个属性,它就必须拿到拥有它的ItemsRepeater并自行监听其EffectiveViewportChanged事件——这会造成不必要的强耦合依赖。将可见视口通过VirtualizingLayoutContext提供给布局,既解耦又通用。

VirtualizingLayoutContext.VisibleRectCore 受保护虚方法:自定义上下文如何提供视口

VisibleRectCoreVirtualizingLayoutContext的受保护虚方法,子类重写它来提供VisibleRect属性返回的值:

protected virtual Windows.Foundation.Rect VisibleRectCore();

它在设计上模仿了既有的可重写方法RealizationRectCore()。从 VirtualizingLayoutContext.cpp 可以看出,基类默认实现抛出hresult_not_implemented,即必须由具体上下文子类提供实现。

仓库中ItemsRepeater配套的RepeaterLayoutContext重写了该方法(RepeaterLayoutContext.cpp),直接把ItemsRepeater的可见窗口返回给布局;NonVirtualizingLayoutContext到虚拟化上下文的适配器LayoutContextAdapter也实现了VisibleRectCore(见 LayoutContextAdapter.cpp)。如果你要编写自己的VirtualizingLayoutContext子类(例如为自研宿主控件提供布局上下文),只需按同样的模式重写VisibleRectCore并返回与宿主有效视口对应的矩形。

将两者组合:API 详情与契约定义

从 API 契约(MIDL3)视角看,所有新 API 都挂在既有的LayoutVirtualizingLayoutContext类上,并统一编入Microsoft.UI.Xaml.XamlContract第 5 版契约(见 Microsoft.UI.Xaml.Controls.idl):

namespace Microsoft.UI.Xaml.Controls { [contract(Microsoft.UI.Xaml.XamlContract, 5)] enum IndexBasedLayoutOrientation { None = 0, TopToBottom = 1, LeftToRight = 2, } unsealed runtimeclass Layout : Microsoft.UI.Xaml.DependencyObject { [contract(Microsoft.UI.Xaml.XamlContract, 5)] { IndexBasedLayoutOrientation IndexBasedLayoutOrientation { get; }; protected void SetIndexBasedLayoutOrientation(IndexBasedLayoutOrientation orientation); } } unsealed runtimeclass VirtualizingLayoutContext : LayoutContext { [contract(Microsoft.UI.Xaml.XamlContract, 5)] { Windows.Foundation.Rect VisibleRect { get; }; overridable Windows.Foundation.Rect VisibleRectCore(); } } }

两套 API 各司其职、相互配合,构成了"布局 → 消费方控件"之间的两个关键信息通道:

|API 组合|解决的问题|典型消费者| |-|-|-| |IndexBasedLayoutOrientation/SetIndexBasedLayoutOrientation| 索引与空间方向的关系,支撑方向键键盘导航 |ItemsViewTryGetItemIndex、键盘导航逻辑,见 ItemsView.cpp 与 ItemsViewInteractions.cpp) | |VisibleRect/VisibleRectCore| 可见视口信息,支撑虚拟化元素按需加载与行冻结 |LinedFlowLayout等虚拟化布局(见 LinedFlowLayout.cpp) |

实战要点与最佳实践

结合设计文档与仓库实现,使用这些 API 时有以下几点值得遵循:

  1. 自定义布局务必声明方向:如果你的布局具有"索引递增对应某一固定方向"的特征(横向如LeftToRight、纵向如TopToBottom),应在构造函数或其他初始化路径中调用SetIndexBasedLayoutOrientation。参考LinedFlowLayout在 LinedFlowLayout.cpp 的写法。若布局形态与索引无固定关系(如自由流动的瀑布流),则保持默认None即可——此时消费方控件会退回纯物理位置导航,行为依然正确。

  2. IndexBasedLayoutOrientation只影响消费方,不影响布局自身排列:不要指望设置该属性会改变Layout的 Measure/Arrange 行为,它只是一份供ItemsView这类控件读取的声明。

  3. 虚拟化布局应优先使用context.VisibleRect()而非直接依赖宿主:不要自行获取ItemsRepeater再监听EffectiveViewportChanged,那是设计文档明确指出的反模式(不必要的依赖耦合)。通过VirtualizingLayoutContext.VisibleRect获取可见视口,再结合RealizationRect(实现窗口)规划实现范围,是仓库中LinedFlowLayout的标准做法。

  4. 自定义VirtualizingLayoutContext必须重写VisibleRectCore:基类默认实现会抛出hresult_not_implemented,请参照 RepeaterLayoutContext.cpp 返回宿主控件的可见窗口矩形。

总结

IndexBasedLayoutOrientationVisibleRect这两组 API 共同补齐了 WinUI 布局体系的两个关键信息缺口:前者让ItemsView能把键盘导航从"物理位置猜测"升级为"索引与方向明确关联"的确定性行为,后者让LinedFlowLayout这类虚拟化布局能够直接获取可见视口,从而精准实现行冻结与按需加载。它们的核心设计原则是解耦——布局只需声明自身特性、消费方控件负责读取决策,互不侵入。若希望继续深入,可阅读完整的 layout-updates-for-lfl.md 设计文档,并对照 LinedFlowLayout.cpp、ItemsView.cpp 与 LinedFlowLayoutTests.cs 理解其完整实现与验证路径。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询