原文:【WPF】UI虚拟化之------自定义VirtualizingWrapPanel

前言

前几天QA报了一个关于OOM的bug,在排查的过程中发现,ListBox控件中被塞入了过多的Item,而ListBox又定义了两种样式的ItemsPanelTemplate。一种用的是虚拟化的VirtualizingStackPanel,另一种没有考虑虚拟化用的是WrapPanel。所以当ListBox切换到第二种Template,而且有很多Item的时候,内存就爆掉然后直接挂了。

然后就想着有没有现成的VirtualizingWrapPanel可以直接拿来用用,可惜微软并没有直接给我们提供这种panel,但是提供了VirtualizingPanel这个抽象类。没办法只能自己动手做了,借助于VirtualizingPanelIScrollInfoIScrollInfo主要是用来滚动效果,而VirtualizingPanel则提供了虚拟化过程中,child的移除和添加操作。其实虚拟化的本质不就是把需要显示到UI上的item画上去,把已经画上去但不需要再显示的撤下来嘛!

因为改bug的时候又来了个新的需求,就是要把WrapPanel中每一行的item之间的距离设置为等间距的,所以这次的UI虚拟化之旅确切来说应该是自定义一个VirtualizingUniformGridWrapPanel。

实现

1. 新建类VirtualizingWrapPanel,继承VirtualizingPanel并实现IScrollInfo

  1. public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo
  2. {
  3. }

然后添加一个TranslateTransform字段,这主要是滚动时需要用到。

  1. private TranslateTransform trans = new TranslateTransform();

接下来添加几个依赖属性,设置内部Child的宽、高和鼠标滚动一次的偏移量。

  1. public static readonly DependencyProperty ChildWidthProperty = DependencyProperty.RegisterAttached("ChildWidth", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
  2. public static readonly DependencyProperty ChildHeightProperty = DependencyProperty.RegisterAttached("ChildHeight", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
  3. //鼠标每一次滚动 UI上的偏移
  4. public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.RegisterAttached("ScrollOffset", typeof(int), typeof(VirtualizingWrapPanel), new PropertyMetadata(10));
  5. public int ScrollOffset
  6. {
  7. get { return Convert.ToInt32(GetValue(ScrollOffsetProperty)); }
  8. set { SetValue(ScrollOffsetProperty, value); }
  9. }
  10. public double ChildWidth
  11. {
  12. get => Convert.ToDouble(GetValue(ChildWidthProperty));
  13. set => SetValue(ChildWidthProperty, value);
  14. }
  15. public double ChildHeight
  16. {
  17. get => Convert.ToDouble(GetValue(ChildHeightProperty));
  18. set => SetValue(ChildHeightProperty, value);
  19. }

2. 理解WPF中的布局定位流程

WPF中布局定位的计算是通过Measure和Arrange方法构成的。以VirtualizingWrapPanel为例(以下简称VWP),VWP的父layout调用自身的Measure(Size availableSize) 方法,告诉VWP你有availableSize的大小可以使用,然后MeasureCore会根据一定的测量逻辑,告诉VWP的protected override Size MeasureOverride(Size availableSize) 方法,你有availableSize的大小可以用,在这里VWP调用其子元素的Measure方法,告诉子元素有多大的Size可以用(此例,因为我们子child的大小都是通过依赖属性设置好的,所以直接传入即可,子child的DesiredSize也不考虑)。子child都Measure完之后,返回一个Size,这个Size是VMP自身需要的Size,父layout会通过VWP.DesiredSize属性拿到这个值。然后ArrangeCore又会根据一定的逻辑,分配一个finalSize给VWP。VWP通过protected override Size ArrangeOverride(Size finalSize)方法就收到了这个值,然后在给定的finalSize里划分不同的区域,调用子child的Arrange方法,告诉每个child应该在哪个区域。

  1. /// <summary>
  2. /// scroll/availableSize/添加删除元素 改变都会触发 edit元素不会改变
  3. /// </summary>
  4. /// <param name="availableSize"></param>
  5. /// <returns></returns>
  6. protected override Size MeasureOverride(Size availableSize)
  7. {
  8. this.UpdateScrollInfo(availableSize);//availableSize更新后,更新滚动条
  9. int firstVisiableIndex = 0, lastVisiableIndex = 0;
  10. //availableSize更新后,获取当前viewport内可放置的item的开始和结束索引,
  11. //firstIdnex-lastIndex之间的item可能部分在viewport中也可能都不在viewport中。
  12. GetVisiableRange(ref firstVisiableIndex, ref lastVisiableIndex);
  13. //因为配置了虚拟化,所以children的个数一直是viewport区域内的个数,
  14. //如果没有虚拟化则是ItemSource的整个的个数
  15. UIElementCollection children = this.InternalChildren;
  16. IItemContainerGenerator generator = this.ItemContainerGenerator;
  17. //获得第一个可被显示的item的位置
  18. GeneratorPosition startPosi = generator.GeneratorPositionFromIndex(firstVisiableIndex);
  19. int childIndex = (startPosi.Offset == 0) ? startPosi.Index : startPosi.Index + 1;//startPosi在chilren中的索引
  20. using (generator.StartAt(startPosi, GeneratorDirection.Forward, true))
  21. {
  22. int itemIndex = firstVisiableIndex;
  23. //生成lastVisiableIndex-firstVisiableIndex个item
  24. while (itemIndex <= lastVisiableIndex)
  25. {
  26. bool newlyRealized = false;
  27. var child = generator.GenerateNext(out newlyRealized) as UIElement;
  28. if (newlyRealized)
  29. {
  30. if (childIndex >= children.Count)
  31. base.AddInternalChild(child);
  32. else
  33. {
  34. base.InsertInternalChild(childIndex, child);
  35. }
  36. generator.PrepareItemContainer(child);
  37. }
  38. else
  39. {
  40. if (!child.Equals(children[childIndex]))
  41. {
  42. base.RemoveInternalChildRange(childIndex, 1);
  43. }
  44. }
  45. child.Measure(new Size(this.ChildWidth, this.ChildHeight));
  46. //child.DesiredSize//child想要的size
  47. itemIndex++;
  48. childIndex++;
  49. }
  50. }
  51. CleanUpItems(firstVisiableIndex, lastVisiableIndex);
  52. return new Size(double.IsInfinity(availableSize.Width) ? 0 : availableSize.Width, double.IsInfinity(availableSize.Height) ? 0 : availableSize.Height);//自身想要的size
  53. }
  54. protected override Size ArrangeOverride(Size finalSize)
  55. {
  56. Debug.WriteLine("----ArrangeOverride");
  57. var generator = this.ItemContainerGenerator;
  58. UpdateScrollInfo(finalSize);
  59. int childPerRow = CalculateChildrenPerRow(finalSize);
  60. double availableItemWidth = finalSize.Width / childPerRow;
  61. for (int i = 0; i <= this.Children.Count - 1; i++)
  62. {
  63. var child = this.Children[i];
  64. int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
  65. int row = itemIndex / childPerRow;//current row
  66. int column = itemIndex % childPerRow;
  67. double xCorrdForItem = 0;
  68. xCorrdForItem = column * availableItemWidth + (availableItemWidth - this.ChildWidth) / 2;
  69. Rect rec = new Rect(xCorrdForItem, row * this.ChildHeight, this.ChildWidth, this.ChildHeight);
  70. child.Arrange(rec);
  71. }
  72. return finalSize;
  73. }

3. 什么时候应该刷新UI

MSDN文档 告诉我们,当ScrollViewer的offset, extent, or viewport 这三个属性发生变化时,应该当调用ScrollViewer的InvalidateScrollInfo方法,然后ScrollView就会自动更新滚动条长短和位置。此时也应该调用InvalidateMeasure方法,然后会重新Measure布局。

offset,extent和viewport的表示区域如下图:



黑色的表示实际显示到界面上的内容。如果不虚拟化则24个item都会在wrappanel中,虚拟化后只有需要显示的那部分(9-16)会在wrappanel中,其他的都删除了。

更新UI操作:

  1. public void SetVerticalOffset(double offset)
  2. {
  3. if (offset < 0 || this.viewPort.Height >= this.extent.Height)
  4. offset = 0;
  5. else
  6. if (offset + this.viewPort.Height >= this.extent.Height)
  7. offset = this.extent.Height - this.viewPort.Height;
  8. this.offset.Y = offset;
  9. this.ScrollOwner?.InvalidateScrollInfo();//Scroll信息已过期
  10. this.trans.Y = -offset;
  11. this.InvalidateMeasure();//Measure信息已过期
  12. //接下来会触发MeasureOverride()
  13. }

4. 虚拟化操作

操作的第一步是获取当前VWP中已加载的所有的child和ListBox的数据源中所有的child。

VWP中children的获取可通过this.InternalChildren拿到。

数据源中children包含在this.ItemContainerGenerator 里面。

这里sdk有个bug,如果你不先调用this.InternalChildren,直接用ItemContainerGenerator后续生成child操作会返回null。

第二步,获取到应该显示到viewport区域内的第一个child和最后一个child的索引,此时viewport的大小可能已经是变化后的。(因为你可能滚动了鼠标,或者更改了VWP的宽高)

  1. /// <summary>
  2. /// 获取所有item,在可视区域内第一个item和最后一个item的索引
  3. /// </summary>
  4. /// <param name="firstIndex"></param>
  5. /// <param name="lastIndex"></param>
  6. void GetVisiableRange(ref int firstIndex, ref int lastIndex)
  7. {
  8. int childPerRow = CalculateChildrenPerRow(this.extent);
  9. firstIndex = Convert.ToInt32(Math.Floor(this.offset.Y / this.ChildHeight)) * childPerRow;
  10. lastIndex = Convert.ToInt32(Math.Ceiling((this.offset.Y + this.viewPort.Height) / this.ChildHeight)) * childPerRow - 1;
  11. int itemsCount = GetItemCount(this);
  12. if (lastIndex >= itemsCount)
  13. lastIndex = itemsCount - 1;
  14. }

第三、通过Generator生成从firstIndex到lastIndex的项,并添加到ListBox(等ItemsControl中)。

在开始之前先看几个定义和工作流程:

  • 定义:在这里我们把你绑定到ItemsControl上的数据称作DataItems,其子项称为DataItem。
  • 定义:把在UI上的Itemscontrol空间内的item称为UIItem,UIItem放到不同的control里有不同的名字,如ListViewItemTreeViewItem等。
  • 获取generatorPostition流程:对应的是generator.GeneratorPositionFromIndex(dataItemIndex)方法,根据dataItemIndex从DataItems里找到对应的DataItem,然后再根据DataItem获取到它在generator里的索引。
  • GeneratorPosition类:有两个属性index和offset。当offset==0时,表示此DataItem被Realized过,即存在对应的UIItem,index就是UIItem在generator中的位置索引。当offset!=0时,表示此DataItem是Virtualized的,没有被Realized过,此时index是-1。(不知道理解是否有误)
  • 生成流程:generator负责把DataItem加工成UIItem并显示(添加)到ItemsControl上。对应的是generator.GenerateNext()方法,返回值是一个UIElement(也就是UIItem),并把它添加/插入到Children中,并为它准备好容器PrepareItemContainer
  • 其他:listBox.ItemContainerGenerator几个方法的对比:
    • ListBox.ItemContainerGenerator.ContainerFromIndex():通过DataItem在DataItems里的index,查到在ItemContainerGenerator中对应的UIItem。
    • ListBox.ItemContainerGenerator.ContainerFromItem(): 通过DataItem,查找在ItemContainerGenerator中对应的Item。
    • generator.GeneratorPositionFromIndex():通过DataItem在DataItems里的index,获取它在generator里的位置。
  1. GeneratorPosition startPosi = generator.GeneratorPositionFromIndex(firstVisiableIndex);
  2. int childIndex = (startPosi.Offset == 0) ? startPosi.Index : startPosi.Index + 1;
  3. using (generator.StartAt(startPosi, GeneratorDirection.Forward, true))
  4. {
  5. int itemIndex = firstVisiableIndex;
  6. while (itemIndex <= lastVisiableIndex)
  7. {
  8. bool newlyRealized = false;
  9. //不断的从DataItems中获取DataItem并生成UIItem
  10. var child = generator.GenerateNext(out newlyRealized) as UIElement;
  11. if (newlyRealized)
  12. {
  13. if (childIndex >= children.Count)
  14. {
  15. base.AddInternalChild(child);
  16. }
  17. else
  18. {
  19. base.InsertInternalChild(childIndex, child);
  20. }
  21. generator.PrepareItemContainer(child);
  22. }
  23. else
  24. {//generator里已经有了
  25. if (!child.Equals(children[childIndex]))
  26. {
  27. //不相等表示children[childIndex]对应的DataItem已经不在DataItems里了,所以在Children里也要删除。
  28. base.RemoveInternalChildRange(childIndex, 1);
  29. }
  30. }
  31. child.Measure(new Size(this.ChildWidth, this.ChildHeight));
  32. itemIndex++;
  33. childIndex++;
  34. }
  35. }

第四、将VWP中已不需在viewport内显示的child从children和generator的container中移除。(generator里的子项个数就是children里的子项个数。)

  1. /// <summary>
  2. /// 将不在可视区域内的item 移除
  3. /// </summary>
  4. /// <param name="startIndex">可视区域开始索引</param>
  5. /// <param name="endIndex">可视区域结束索引</param>
  6. void CleanUpItems(int startIndex, int endIndex)
  7. {
  8. var children = this.InternalChildren;
  9. var generator = this.ItemContainerGenerator;
  10. for (int i = children.Count - 1; i >= 0; i--)
  11. {
  12. var childGeneratorPosi = new GeneratorPosition(i, 0);
  13. int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPosi);
  14. if (itemIndex < startIndex || itemIndex > endIndex)
  15. {
  16. generator.Remove(childGeneratorPosi, 1);
  17. RemoveInternalChildRange(i, 1);
  18. }
  19. }
  20. }

这里虽然调用了generator.Remove方法,将不需要显示的进行移除,但是我发现移除后generator中元素的个数与ListBox绑定的数据源中元素的个数始终是一致的。所以我觉得可能是将次child从generator的container中移除了。因为你从上面的MeasureOverride方法中也看到了,新加时是调用generator的PrepareItemContainer方法。

5. UniformGrid效果的WrapPanel该怎么给child Arrange

其实就在上面的ArrangeOverride方法里,很简单:

  1. xCorrdForItem = column * availableItemWidth + (availableItemWidth - this.ChildWidth) / 2;

6. 存在的Bug或者问题

第一个bug你已经在上面的第4步看过了。

还有个bug是VMP的ScrollOwner报null reference的异常,本质是找不到包裹它的ScrollViewer,其原因可能有两个:

1. 为你的ListBox设置ItemsPanlTempalte是通过Bind的方式,会引发这个异常。如果你尝试着给它Bind一个scrollviewer,那么接下来你可能还会面临着滚动页面出现空白,但是明明新的child已经生成了,就是不会显示的UI上的问题。正确的解决方式是CodeBind,用C#代码为这个ListBox设置ItemsPanlTemplate

2. 自定义了ControlTempalte,就像下面这个代码一样:

  1. <ControlTemplate TargetType="{x:Type ListView}">
  2. <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Visible">
  3. <Border Margin="10">
  4. <ItemsPresenter />
  5. </Border>
  6. </ScrollViewer>
  7. </ControlTemplate>

相当于你定义ControlTempate的时候重新设置了ScrollViewer,然后VMP就找不到了。所以看能不能找到不定义Tempalte的方法吧。

7. 使用方法

  1. <ListBox Margin="0,50,0,0" Name="listB">
  2. <ListBox.ItemTemplate>
  3. <DataTemplate>
  4. <TextBlock Text="{Binding}" Width="70" Height="70"/>
  5. </DataTemplate>
  6. </ListBox.ItemTemplate>
  7. <ListBox.ItemsPanel>
  8. <ItemsPanelTemplate>
  9. <local:VirtualizingWrapPanel ScrollOffset="50" ChildHeight="70" ChildWidth="70"/>
  10. </ItemsPanelTemplate>
  11. </ListBox.ItemsPanel>
  12. </ListBox>

8. 完整代码:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7. using System.Windows;
  8. using System.Windows.Controls;
  9. using System.Windows.Controls.Primitives;
  10. using System.Windows.Media;
  11. namespace VirtualizingPPanel
  12. {
  13. public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo
  14. {
  15. private TranslateTransform trans = new TranslateTransform();
  16. public VirtualizingWrapPanel()
  17. {
  18. this.RenderTransform = trans;
  19. }
  20. #region DependencyProperties
  21. public static readonly DependencyProperty ChildWidthProperty = DependencyProperty.RegisterAttached("ChildWidth", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
  22. public static readonly DependencyProperty ChildHeightProperty = DependencyProperty.RegisterAttached("ChildHeight", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
  23. //鼠标每一次滚动 UI上的偏移
  24. public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.RegisterAttached("ScrollOffset", typeof(int), typeof(VirtualizingWrapPanel), new PropertyMetadata(10));
  25. public int ScrollOffset
  26. {
  27. get { return Convert.ToInt32(GetValue(ScrollOffsetProperty)); }
  28. set { SetValue(ScrollOffsetProperty, value); }
  29. }
  30. public double ChildWidth
  31. {
  32. get => Convert.ToDouble(GetValue(ChildWidthProperty));
  33. set => SetValue(ChildWidthProperty, value);
  34. }
  35. public double ChildHeight
  36. {
  37. get => Convert.ToDouble(GetValue(ChildHeightProperty));
  38. set => SetValue(ChildHeightProperty, value);
  39. }
  40. #endregion
  41. int GetItemCount(DependencyObject element)
  42. {
  43. var itemsControl = ItemsControl.GetItemsOwner(element);
  44. return itemsControl.HasItems ? itemsControl.Items.Count : 0;
  45. }
  46. int CalculateChildrenPerRow(Size availableSize)
  47. {
  48. int childPerRow = 0;
  49. if (availableSize.Width == double.PositiveInfinity)
  50. childPerRow = this.Children.Count;
  51. else
  52. childPerRow = Math.Max(1, Convert.ToInt32(Math.Floor(availableSize.Width / this.ChildWidth)));
  53. return childPerRow;
  54. }
  55. /// <summary>
  56. /// width不超过availableSize的情况下,自身实际需要的Size(高度可能会超出availableSize)
  57. /// </summary>
  58. /// <param name="availableSize"></param>
  59. /// <param name="itemsCount"></param>
  60. /// <returns></returns>
  61. Size CalculateExtent(Size availableSize, int itemsCount)
  62. {
  63. int childPerRow = CalculateChildrenPerRow(availableSize);//现有宽度下 一行可以最多容纳多少个
  64. return new Size(childPerRow * this.ChildWidth, this.ChildHeight * Math.Ceiling(Convert.ToDouble(itemsCount) / childPerRow));
  65. }
  66. /// <summary>
  67. /// 更新滚动条
  68. /// </summary>
  69. /// <param name="availableSize"></param>
  70. void UpdateScrollInfo(Size availableSize)
  71. {
  72. var extent = CalculateExtent(availableSize, GetItemCount(this));//extent 自己实际需要
  73. if (extent != this.extent)
  74. {
  75. this.extent = extent;
  76. this.ScrollOwner.InvalidateScrollInfo();
  77. }
  78. if (availableSize != this.viewPort)
  79. {
  80. this.viewPort = availableSize;
  81. this.ScrollOwner.InvalidateScrollInfo();
  82. }
  83. }
  84. /// <summary>
  85. /// 获取所有item,在可视区域内第一个item和最后一个item的索引
  86. /// </summary>
  87. /// <param name="firstIndex"></param>
  88. /// <param name="lastIndex"></param>
  89. void GetVisiableRange(ref int firstIndex, ref int lastIndex)
  90. {
  91. int childPerRow = CalculateChildrenPerRow(this.extent);
  92. firstIndex = Convert.ToInt32(Math.Floor(this.offset.Y / this.ChildHeight)) * childPerRow;
  93. lastIndex = Convert.ToInt32(Math.Ceiling((this.offset.Y + this.viewPort.Height) / this.ChildHeight)) * childPerRow - 1;
  94. int itemsCount = GetItemCount(this);
  95. if (lastIndex >= itemsCount)
  96. lastIndex = itemsCount - 1;
  97. }
  98. /// <summary>
  99. /// 将不在可视区域内的item 移除
  100. /// </summary>
  101. /// <param name="startIndex">可视区域开始索引</param>
  102. /// <param name="endIndex">可视区域结束索引</param>
  103. void CleanUpItems(int startIndex, int endIndex)
  104. {
  105. var children = this.InternalChildren;
  106. var generator = this.ItemContainerGenerator;
  107. for (int i = children.Count - 1; i >= 0; i--)
  108. {
  109. var childGeneratorPosi = new GeneratorPosition(i, 0);
  110. int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPosi);
  111. if (itemIndex < startIndex || itemIndex > endIndex)
  112. {
  113. generator.Remove(childGeneratorPosi, 1);
  114. RemoveInternalChildRange(i, 1);
  115. }
  116. }
  117. }
  118. /// <summary>
  119. /// scroll/availableSize/添加删除元素 改变都会触发 edit元素不会改变
  120. /// </summary>
  121. /// <param name="availableSize"></param>
  122. /// <returns></returns>
  123. protected override Size MeasureOverride(Size availableSize)
  124. {
  125. this.UpdateScrollInfo(availableSize);//availableSize更新后,更新滚动条
  126. int firstVisiableIndex = 0, lastVisiableIndex = 0;
  127. GetVisiableRange(ref firstVisiableIndex, ref lastVisiableIndex);//availableSize更新后,获取当前viewport内可放置的item的开始和结束索引 firstIdnex-lastIndex之间的item可能部分在viewport中也可能都不在viewport中。
  128. UIElementCollection children = this.InternalChildren;//因为配置了虚拟化,所以children的个数一直是viewport区域内的个数,如果没有虚拟化则是ItemSource的整个的个数
  129. IItemContainerGenerator generator = this.ItemContainerGenerator;
  130. //获得第一个可被显示的item的位置
  131. GeneratorPosition startPosi = generator.GeneratorPositionFromIndex(firstVisiableIndex);
  132. int childIndex = (startPosi.Offset == 0) ? startPosi.Index : startPosi.Index + 1;//startPosi在chilren中的索引
  133. using (generator.StartAt(startPosi, GeneratorDirection.Forward, true))
  134. {
  135. int itemIndex = firstVisiableIndex;
  136. while (itemIndex <= lastVisiableIndex)//生成lastVisiableIndex-firstVisiableIndex个item
  137. {
  138. bool newlyRealized = false;
  139. var child = generator.GenerateNext(out newlyRealized) as UIElement;
  140. if (newlyRealized)
  141. {
  142. if (childIndex >= children.Count)
  143. base.AddInternalChild(child);
  144. else
  145. {
  146. base.InsertInternalChild(childIndex, child);
  147. }
  148. generator.PrepareItemContainer(child);
  149. }
  150. else
  151. {
  152. //处理 正在显示的child被移除了这种情况
  153. if (!child.Equals(children[childIndex]))
  154. {
  155. base.RemoveInternalChildRange(childIndex, 1);
  156. }
  157. }
  158. child.Measure(new Size(this.ChildWidth, this.ChildHeight));
  159. //child.DesiredSize//child想要的size
  160. itemIndex++;
  161. childIndex++;
  162. }
  163. }
  164. CleanUpItems(firstVisiableIndex, lastVisiableIndex);
  165. return new Size(double.IsInfinity(availableSize.Width) ? 0 : availableSize.Width, double.IsInfinity(availableSize.Height) ? 0 : availableSize.Height);//自身想要的size
  166. }
  167. protected override Size ArrangeOverride(Size finalSize)
  168. {
  169. Debug.WriteLine("----ArrangeOverride");
  170. var generator = this.ItemContainerGenerator;
  171. UpdateScrollInfo(finalSize);
  172. int childPerRow = CalculateChildrenPerRow(finalSize);
  173. double availableItemWidth = finalSize.Width / childPerRow;
  174. for (int i = 0; i <= this.Children.Count - 1; i++)
  175. {
  176. var child = this.Children[i];
  177. int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
  178. int row = itemIndex / childPerRow;//current row
  179. int column = itemIndex % childPerRow;
  180. double xCorrdForItem = 0;
  181. xCorrdForItem = column * availableItemWidth + (availableItemWidth - this.ChildWidth) / 2;
  182. Rect rec = new Rect(xCorrdForItem, row * this.ChildHeight, this.ChildWidth, this.ChildHeight);
  183. child.Arrange(rec);
  184. }
  185. return finalSize;
  186. }
  187. protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
  188. {
  189. base.OnRenderSizeChanged(sizeInfo);
  190. this.SetVerticalOffset(this.VerticalOffset);
  191. }
  192. protected override void OnClearChildren()
  193. {
  194. base.OnClearChildren();
  195. this.SetVerticalOffset(0);
  196. }
  197. protected override void BringIndexIntoView(int index)
  198. {
  199. if (index < 0 || index >= Children.Count)
  200. throw new ArgumentOutOfRangeException();
  201. int row = index / CalculateChildrenPerRow(RenderSize);
  202. SetVerticalOffset(row * this.ChildHeight);
  203. }
  204. #region IScrollInfo Interface
  205. public bool CanVerticallyScroll { get; set; }
  206. public bool CanHorizontallyScroll { get; set; }
  207. private Size extent = new Size(0, 0);
  208. public double ExtentWidth => this.extent.Width;
  209. public double ExtentHeight => this.extent.Height;
  210. private Size viewPort = new Size(0, 0);
  211. public double ViewportWidth => this.viewPort.Width;
  212. public double ViewportHeight => this.viewPort.Height;
  213. private Point offset;
  214. public double HorizontalOffset => this.offset.X;
  215. public double VerticalOffset => this.offset.Y;
  216. public ScrollViewer ScrollOwner { get; set; }
  217. public void LineDown()
  218. {
  219. this.SetVerticalOffset(this.VerticalOffset + this.ScrollOffset);
  220. }
  221. public void LineLeft()
  222. {
  223. throw new NotImplementedException();
  224. }
  225. public void LineRight()
  226. {
  227. throw new NotImplementedException();
  228. }
  229. public void LineUp()
  230. {
  231. this.SetVerticalOffset(this.VerticalOffset - this.ScrollOffset);
  232. }
  233. public Rect MakeVisible(Visual visual, Rect rectangle)
  234. {
  235. return new Rect();
  236. }
  237. public void MouseWheelDown()
  238. {
  239. this.SetVerticalOffset(this.VerticalOffset + this.ScrollOffset);
  240. }
  241. public void MouseWheelLeft()
  242. {
  243. throw new NotImplementedException();
  244. }
  245. public void MouseWheelRight()
  246. {
  247. throw new NotImplementedException();
  248. }
  249. public void MouseWheelUp()
  250. {
  251. this.SetVerticalOffset(this.VerticalOffset - this.ScrollOffset);
  252. }
  253. public void PageDown()
  254. {
  255. this.SetVerticalOffset(this.VerticalOffset + this.viewPort.Height);
  256. }
  257. public void PageLeft()
  258. {
  259. throw new NotImplementedException();
  260. }
  261. public void PageRight()
  262. {
  263. throw new NotImplementedException();
  264. }
  265. public void PageUp()
  266. {
  267. this.SetVerticalOffset(this.VerticalOffset - this.viewPort.Height);
  268. }
  269. public void SetHorizontalOffset(double offset)
  270. {
  271. throw new NotImplementedException();
  272. }
  273. public void SetVerticalOffset(double offset)
  274. {
  275. if (offset < 0 || this.viewPort.Height >= this.extent.Height)
  276. offset = 0;
  277. else
  278. if (offset + this.viewPort.Height >= this.extent.Height)
  279. offset = this.extent.Height - this.viewPort.Height;
  280. this.offset.Y = offset;
  281. this.ScrollOwner?.InvalidateScrollInfo();
  282. this.trans.Y = -offset;
  283. this.InvalidateMeasure();
  284. //接下来会触发MeasureOverride()
  285. }
  286. #endregion
  287. }
  288. }

9. Demo下载

最终效果:

下载:

链接: https://pan.baidu.com/s/1jHMBFM2 密码: 4csp

参考

1. Magentaize!——正确实现 WPF 中的 UI 虚拟化

2. GitHub - digimezzo/WPFControls: WPF Controls

3.WPF布局

4.Implementing a VirtualizingPanel part 3: MeasureCore

【WPF】UI虚拟化之------自定义VirtualizingWrapPanel的更多相关文章

  1. WPF UI虚拟化

    ComboBox

  2. wpf 客户端【JDAgent桌面助手】开发详解(三) 瀑布流效果实现与UI虚拟化优化大数据显示

    目录区域: 业余开发的wpf 客户端终于完工了..晒晒截图 wpf 客户端[JDAgent桌面助手]开发详解-开篇 wpf 客户端[JDAgent桌面助手]详解(一)主窗口 圆形菜单... wpf 客 ...

  3. WPF之UI虚拟化

    在WPF应用程序开发过程中,大数据量的数据展现通常都要考虑性能问题.有下面一种常见的情况:原始数据源数据量很大,但是某一时刻数据容器中的可见元素个数是有限的,剩余大多数元素都处于不可见状态,如果一次性 ...

  4. WPF的UI虚拟化

    许多时候,我们的界面上会呈现大量的数据,如包含数千条记录的表格或包含数百张照片的相册.由于呈现UI是一件开销比较大的动作,一次性呈现数百张照片就目前的电脑性能来说是需要占用大量内存和时间的.因此需要对 ...

  5. WPF listbox UI虚拟化

    ListBox  默认是UI虚拟化的. 1. 原生使用  <ListBox VirtualizingPanel.IsVirtualizing="True" Virtualiz ...

  6. 精通 WPF UI Virtualization (提升 OEA 框架中 TreeGrid 控件的性能)

    原文:精通 WPF UI Virtualization (提升 OEA 框架中 TreeGrid 控件的性能) 本篇博客主要说明如何使用 UI Virtualization(以下简称为 UIV) 来提 ...

  7. OpenExpressApp:精通 WPF UI Virtualization

    原文:OpenExpressApp:精通 WPF UI Virtualization 本篇博客主要说明如何使用 UI Virtualization(以下简称为 UIV) 来提升 OEA 框架中 Tre ...

  8. WPF之路——实现自定义虚拟容器(实现VirtualizingPanel)

    原文:WPF之路--实现自定义虚拟容器(实现VirtualizingPanel) 源码下载地址: http://download.csdn.net/detail/qianshen88/6618033 ...

  9. windowsphone 瀑布流&ui虚拟化

    瀑布流已经有点年代了吧,不过wp上还真是挺少资料的.今天抽空把自己之前搞过的东西写出来,避免大家重复劳动. 一.简单的瀑布流排版加入ui虚拟化. 最近看了 段博琼  ui虚拟化的一篇博文,链接:htt ...

随机推荐

  1. C语言数据类型取值范围解析

    版权声明:本文为博主原创文章,未经博主允许不得转载.   为什么int类型的取值范围会是-2^31 ~ 2^31-1  ,为什么要减一呢? 计算机里规定,8位二进制为一个字节,拿byte来说,一个BY ...

  2. 创建、删除swap分区

    创建 dd if=/dev/zero of=/data/swap bs=1M count=4000 mkswap  /data/swap  swapon   /data/swap  chmod 060 ...

  3. C++ 程序延时处理的几种方法

    (—)使用_sleep()函数 例如:_sleep(200);//延时200毫秒 (二)使用delay(int time)函数 (需要自己实现,编译器里面没有) /// @brief      程序延 ...

  4. php课程 4-15 数组遍历、超全局数组、表单提交数据(多看学习视频)

    php课程 4-15  数组遍历.超全局数组.表单提交数据(多看学习视频) 一.总结 一句话总结:超全局数组特别有用,比如$_SERVER可以获取所有的客户端访问服务器的情况. 1.数组遍历三种方式( ...

  5. Delphi程序的自我修改

    前言:     对于Delphi在编译时对代码所做的工作,大部分使用Object Pascal之类的高级语言的程序员并不是很熟悉.如果你对汇编程序以及EXE文件格式有一点基本认识,那么源代码里包含的注 ...

  6. 强大的 function adapters

    void printElem(int elem, const char* prefix){ cout << prefix << elem << endl; } fo ...

  7. System.nanoTime()和System.currentTimeMillis()性能问题

    ​ 之前给模块做性能优化的时候,需要将性能调到毫秒级,使用了System.nanoTime()和System.currentTimeMillis()对代码分片计时分析耗时操作,后发现在串行情况下性能达 ...

  8. Node.js开发入门—使用AngularJS

    做一个Web应用,一般都有前台和后台,Node.js能够实现后台.利用jade模板引擎也能够生成一些简单的前台页面,但要想开发出具有实际意义的现代Web应用.还得搭配一个Web前端框架. Angula ...

  9. python 爬取bilibili 视频弹幕

    # -*- coding: utf-8 -*- # @author: Tele # @Time : 2019/04/09 下午 4:50 # 爬取弹幕 import requests import j ...

  10. 谈谈android缓存文件

    ##内部存储 总是可用的 这里的文件默认是只能被你的app所访问的. 当用户卸载你的app的时候,系统会把internal里面的相关文件都清除干净. Internal是在你想确保不被用户与其他app所 ...