本文内容参考微软文档
列表视图和网格视图 – Windows apps | Microsoft Learn
ListView 和 GridView UI 优化 – UWP applications | Microsoft Learn
1、WinUI 3中的集合控件
WinUI 3中提供了几种不同的集合控件,包括ListView、GridView、ListBox、ItemsView、ItemsRepeater、ItemsControl,这几种控件都是用来显示集合的,功能看起来类似,可能很多人会不知道该选用哪种控件,这里简单说一下。
ListView和GridView都是继承于ListViewBase类,都提供有表头表尾等功能,区别在于二者呈现集合的布局。ListView主要呈现方式为垂直排列,相当于是正常的列表,适用于歌单、联系人等场景;GridView主要呈现方式为网格布局,它的呈现方式类似于Grid布局,从左到右依次排列,一行放满了就添加一行继续排列,适用于卡片排列、图库、产品库等场景。
需要注意的是,在GridView中,如果一行中剩余的控件不够一个元素排布,那么就会把这些空间空出来,并不会自动放缩已有元素来把空间占满,若要达到这种效果,UWP可以使用Windows Community Toolkit提供的AdaptGridView(7.x版本提供,8.0开始似乎不再提供,可采用WinUI 3的方式继续使用),WinUI 3中Windows Community Toolkit不提供此控件,但可直接复制AdaptGridView源码使用,目前未发现严重兼容问题。
ListBox虽然与ListView相似,但是它主要作用实际上是为下拉框等场景提供呈现组件,并不能很好地处理大量复杂数据,因此不推荐作为列表控件使用,仅在需要显示少量元素时使用最好。
ItemsView相比ListView和GridView提供了更加灵活的布局方式,它允许你更换多种不同的布局,换句话说支持你自己实现布局(只要你实现了Layout 类),目前已提供了多种布局,支持虚拟化的布局有LinedFlowLayout、StackLayout、UniformGridLayout,不支持虚拟化的布局有ColumnMajorUniformToLargestGridLayout,具体的布局实现可参考Layout 类API文档中的“派生”部分。
Items
ItemsControl同ItemsRepeater类似,都是更加轻量的存在,它是ListView、GridView、ListBox、其他 Selector 派生控件 (ComboBox、 FlipView) 、 MenuFlyoutPresenter等控件的基类,虽然是这些集合的基类,但是它自身也可直接使用,直接使用时则不支持虚拟化,建议还是使用ItemsRepeater。
虚拟化 | 选择 SelectionMode | 点击元素 IsItemClickEnabled | 更换布局 Layout | 渐进更新 | |
Items | ✅ | ❌ | ❌ | ✅ | ❌ |
ItemsView | ✅ | ✅ | ✅ (IsItemInvokedEnabled) | ✅ | ❌ |
ItemsControl | ❌ | ❌ | ❌ | ❌ | ❌ |
ListBox | ✅ | ✅ | ❌ | ❌ | ❌ |
ListView | ✅ | ✅ | ✅ | ❌ | ✅ |
GridView | ✅ | ✅ | ✅ | ❌ | ✅ |
注意,虽然ItemsControl并不支持更换布局,但是它提供了ItemsPanel属性用于修改元素的布局,也能在一定程度上更换布局,只是该属性要求提供的是Panel的派生类,因此相比较ItemsView等灵活性不是特别大。
2、UI虚拟化
UI虚拟化简单来说指的是只渲染可见范围内的元素,不在可见范围内的元素会被删除或者重复利用,在UI界面开发中非常常用,在面对大批量数据时可以节省程序所占用的内存,是性能优化中最佳的手段之一。
本文主要讲ListView、GridView的性能优化,其它控件同理,后文统称容器,对于具体的控件则会单独说明。
WinUI 3中很多集合控件都支持UI虚拟化,且默认启用,因此不需要额外配置即可使用。但是,在某些情况下,虚拟化可能会失效,因此要确保编写的代码不会触发虚拟化失效才能正常利用UI虚拟化。
如前所述,UI虚拟化只会渲染可见范围内的元素,实际上为了用户体验,还会包括当前可见范围的前后一段范围内的元素也会被保留和渲染。有一个专门的概念来描述可见范围——视口(ViewPort)。在元素即将进入视口时,程序会自动创建对应的实际元素并开始渲染,在元素离开视口后,并且不太可能很快就再次出现时,对应的实际元素将会被删除。
为了确定视口,程序会自动根据容器的宽高进行计算,这里就是一个常见的导致虚拟化失效的地方了。有些面板允许子元素拥有无限的宽度或高度,例如ScrollViewer,这样一来,容器的宽度或高度为无限大,视口自然也被认为是无限大,于是容器内所有的元素都被认为在视口内,从而会把所有的实际元素渲染出来,此时虚拟化就失效了。因此,务必要确保容器有确定的宽度和高度,并且不会过度超出实际呈现的区域。
此外,若要提供其它面板(ItemsPanel)修改子元素布局,则必须使用虚拟化面板,例如ItemsWrapGrid或ItemsStackPanel。如果使用 VariableSizedWrapGrid、WrapGrid或 StackPanel,则不会实现虚拟化。
另外,某些元素的控件涉及到图片或其它可变宽高的控件,导致无法确定具体大小,因此可能也会导致计算出的视口内所需元素数量过大进而浪费内存,推荐最好能提供具体大小或是最大/最小大小。
此外,还有两种不同的虚拟化策略,即增量加载或分页查看,它们的核心思想都是减少一次性加载的元素数量,从而减少资源占用,提高程序性能。这两种虚拟化策略和上述的虚拟化并不冲突,可叠加使用,此处由于篇幅问题不再过多赘述,可参考《ListView 和 GridView 数据虚拟化》,WinUI 3与此文档所述内容兼容,可直接使用,其中增量加载对应《增量数据虚拟化》部分,分页查看对应《随机访问数据虚拟化》部分。
3、渐进更新
ContainerContentChanging是由ListViewBase提供的功能,它允许分阶段渲染容器中的实际元素,提升加载速度,实现快速平滑滚动。但仅当ListView或GridView的ItemsPanel为ItemsStackPanel或ItemsWrapGrid时,才会引发此事件。如果将ItemsPanel替换为其它,则不会引发此事件。
分阶段渲染的含义是指,若一个元素中包含有文字、图片或其它需要异步加载的内容,则可指定不同的阶段来渲染不同的内容,例如第一阶段先渲染文字,第二阶段渲染图片,第三阶段加载异步内容,依此类推。这样,当用户快速滚动容器列表时,就不会导致所有内容全部加载和渲染出来,从而可以提高程序的性能,避免大量的渲染和回收。此处的阶段是由开发者自定义的,并不是必须要第一阶段渲染文字、第二阶段渲染图片的意思,阶段的多少也由开发者自定义。
该事件的ContainerContentChangingEventArgs参数中包含有Phase和Handled等参数,其中Phase参数用于获取当前的阶段,是一个uint类型的值,从0开始递增;Handled参数用于标记此事件是否已经被处理,可防止事件路由中的大多数处理程序再次处理同一事件,应该在第一次触发事件时修改为true。ContainerContentChangingEventArgs中还包含了其它内容,可参考文档进行查阅。
另外,ContainerContentChangingEventArgs还包含了RegisterUpdateCallback方法,这个方法可用于注册后续阶段的回调,后续阶段的回调类型与ContainerContentChanging事件处理器一致,从而可以在每个阶段处理完毕后注册下一阶段的处理器。具体代码示例参考此文档,这里不再赘述,文档中的示例已经很完善了。
4、优化元素数量
对于集合控件来说,最好的优化实际上是针对每一项子元素的实际元素控件数量进行优化,若一个元素的实际控件数量为20个,一个300项元素的集合控件实际包含的元素控件数量就超过6000个(不考虑虚拟化情况下),此时若是能够优化每项元素的控件数量,就能够极大减少总计的控件数量。例如,若优化每个元素的实际控件数量到15个,则该集合控件的实际元素控件就能减少到4500个,总计减少了1500个控件,这对于性能的优化非常可观,可以大大提升渲染速度和响应时间。
此外,针对容器中每个元素的实际元素,都对应有一个经过优化的ListViewItemPresenter,若要修改默认的各种样式(例如复选框、背景等),则可使用容器的ItemContainerStyle属性修改,具体可见此文档。