一、虚拟化
WPF列表控件所提供的最重要的功能就是UI虚拟化。
1、UI虚拟化技术其实就是只为可见区域中能显示的项创建容器对象的一种技术,对提升列表控件的性能有着显卓的效果。假设有一个数万条记录的数据,其可见区域只能展示30条记录仪,此时如果使用虚拟化技术,那么界面只需要创建30个数据(或多几个已保持良好的滑动性能),如果没有使用虚拟化技术,则需要生成属数万个数据 ,这显然会占用更多的内存,且影响应用的流畅度
2、虚拟化的启用
我们需要注意的是WPF控件显示其实是由三部分组成ControlTemplate、DataTemplate、ItemsPanel(有些不一定全部是由这几部分组成这里不做过多解释)
1、ControlTemplate 控制控件展示的外观
2、DataTemplate 数据模板控制控件里面数据展示方式
3、ItemsPanel 控件显示的容器(虚拟化实现的关键) 支持虚拟化的其实是VirtualizingStackPanel容器,此容器控件除了虚拟化支持外,其他的与StackPanel功能是类似的 宁外微软也提供了 VirtualizingPanel, IScrollInfo 自己可以根据自己需求自定义
这里我们说几个常见的设置
1、项容器再循环
列表控件在启用虚拟化的情况下,进行滚动时,默认会创建新展示的子项控件,并销毁离开可视区域的子项控件。可以通过设置VirtualizingStackPanel.VirtualizationMode="Recycling"开启像容器再循环模式,这样列表控件会保留少量的子项控件,在滚动时进行服用,只根据新数据更新其中的内容,而不是每次都创建和销毁。这有助于减少内存使用并提高性能,特别是在滚动大量数据时。
VirtualizingStackPanel.VirtualizationMode的有效值为Standard 和 Recycling,默认情况下为Standard
2、缓存长度
VirtualizingStackPanel会多创建几个超过显示范围的子项,以便在开始滚动时候就可以立即显示这些子项,以此来优化交互。我们可以通过使用VirtualizingStackPanel.CacheLength和VirtualizingStackPanel.CacheLengthUnit来进一步调整数量。
VirtualizingStackPanel.CacheLengthUnit:表示缓存的单位,有效值分别为Item、Page、Pixel,其中Page表示可视窗口所能展示的数量的项,Pixel表示像素,适用于项显示不同大小的图片时。
VirtualizingStackPanel.CacheLength:表示缓存指定单位的数量。
值得注意的是,在滚动时,VirtualizingStackPanel会先将可见的项创建并显示后,再在后台中进行缓存的填充,因此缓存对应用程序流畅度的影响很小。
缓存当前显示项的前一页和后一页
以ItemsControl举例
缓存当前显示项的前一页和后一页
<ItemsControlVirtualizingStackPanel.CacheLength="1" VirtualizingStackPanel.CacheLengthUnit="Page"/>
缓存当前显示项的前100项和后100项
<ItemsControl VirtualizingStackPanel.CacheLength="100" VirtualizingStackPanel.CacheLengthUnit="Item"/>
缓存当前显示项的前100项和后500项
<ItemsControl VirtualizingStackPanel.CacheLength="100,500" VirtualizingStackPanel.CacheLengthUnit="Item" />
最后还有一种默认情况下,当用户在滚动条上拖动滑动块时,会实时刷新列表,为了进一步提高滚动性能,可以通过设置ScrollViewer.IsDeferredScrollingEnabled="True"
开启延迟滚动,
只有当用户释放滚动滑块时才进行刷新。
<ItemsControl ScrollViewer.IsDeferredScrollingEnabled="True"/>
这个特性需要根据实际情况使用了,虽然可以提高性能,但是无法实时展示滑动到哪个位置了,而实际开发时,用户更喜欢能实时看到当前滑动块的展示位置。
滚动单位
VirtualizingStackPanel默认是基于项进行滚动的,也就是说无论是单击滚动条、滚动箭头还是调用ScrollIntoView()方法,在面板上至少会滚动一个完整项,而无法在滚动时展示某个项的一部分。
可以通过VirtualizingStackPanel.ScrollUnlit="Pixel"(默认为Item),将VirtualizingStackPanel的滚动设置为基于像素,可以让滚动更加流畅。
VirtualizingStackPanel.ScrollUnit="Pixel"
接下来我自己实现一个说明一下
新建一个VirtualizingWrapPanell类
public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo { private TranslateTransform trans = new TranslateTransform(); public VirtualizingWrapPanel() { this.RenderTransform = trans; } #region DependencyProperties public static readonly DependencyProperty ChildWidthProperty = DependencyProperty.RegisterAttached("ChildWidth", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange)); public static readonly DependencyProperty ChildHeightProperty = DependencyProperty.RegisterAttached("ChildHeight", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange)); //鼠标每一次滚动 UI上的偏移 public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.RegisterAttached("ScrollOffset", typeof(int), typeof(VirtualizingWrapPanel), new PropertyMetadata(10)); public int ScrollOffset { get { return Convert.ToInt32(GetValue(ScrollOffsetProperty)); } set { SetValue(ScrollOffsetProperty, value); } } public double ChildWidth { get => Convert.ToDouble(GetValue(ChildWidthProperty)); set => SetValue(ChildWidthProperty, value); } public double ChildHeight { get => Convert.ToDouble(GetValue(ChildHeightProperty)); set => SetValue(ChildHeightProperty, value); } #endregion int GetItemCount(DependencyObject element) { var itemsControl = ItemsControl.GetItemsOwner(element); return itemsControl.HasItems ? itemsControl.Items.Count : 0; } int CalculateChildrenPerRow(Size availableSize) { int childPerRow = 0; if (availableSize.Width == double.PositiveInfinity) childPerRow = this.Children.Count; else childPerRow = Math.Max(1, Convert.ToInt32(Math.Floor(availableSize.Width / this.ChildWidth))); return childPerRow; } /// <summary> /// width不超过availableSize的情况下,自身实际需要的Size(高度可能会超出availableSize) /// </summary> /// <param name="availableSize"></param> /// <param name="itemsCount"></param> /// <returns></returns> Size CalculateExtent(Size availableSize, int itemsCount) { int childPerRow = CalculateChildrenPerRow(availableSize);//现有宽度下 一行可以最多容纳多少个 return new Size(childPerRow * this.ChildWidth, this.ChildHeight * Math.Ceiling(Convert.ToDouble(itemsCount) / childPerRow)); } /// <summary> /// 更新滚动条 /// </summary> /// <param name="availableSize"></param> void UpdateScrollInfo(Size availableSize) { var extent = CalculateExtent(availableSize, GetItemCount(this));//extent 自己实际需要 if (extent != this.extent) { this.extent = extent; this.ScrollOwner.InvalidateScrollInfo(); } if (availableSize != this.viewPort) { this.viewPort = availableSize; this.ScrollOwner.InvalidateScrollInfo(); } } /// <summary> /// 获取所有item,在可视区域内第一个item和最后一个item的索引 /// </summary> /// <param name="firstIndex"></param> /// <param name="lastIndex"></param> void GetVisiableRange(ref int firstIndex, ref int lastIndex) { int childPerRow = CalculateChildrenPerRow(this.extent); firstIndex = Convert.ToInt32(Math.Floor(this.offset.Y / this.ChildHeight)) * childPerRow; lastIndex = Convert.ToInt32(Math.Ceiling((this.offset.Y + this.viewPort.Height) / this.ChildHeight)) * childPerRow - 1; int itemsCount = GetItemCount(this); if (lastIndex >= itemsCount) lastIndex = itemsCount - 1; } /// <summary> /// 将不在可视区域内的item 移除 /// </summary> /// <param name="startIndex">可视区域开始索引</param> /// <param name="endIndex">可视区域结束索引</param> void CleanUpItems(int startIndex, int endIndex) { var children = this.InternalChildren; var generator = this.ItemContainerGenerator; for (int i = children.Count - 1; i >= 0; i--) { var childGeneratorPosi = new GeneratorPosition(i, 0); int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPosi); if (itemIndex < startIndex || itemIndex > endIndex) { generator.Remove(childGeneratorPosi, 1); RemoveInternalChildRange(i, 1); } } } /// <summary> /// scroll/availableSize/添加删除元素 改变都会触发 edit元素不会改变 /// </summary> /// <param name="availableSize"></param> /// <returns></returns> protected override Size MeasureOverride(Size availableSize) { this.UpdateScrollInfo(availableSize);//availableSize更新后,更新滚动条 int firstVisiableIndex = 0, lastVisiableIndex = 0; GetVisiableRange(ref firstVisiableIndex, ref lastVisiableIndex);//availableSize更新后,获取当前viewport内可放置的item的开始和结束索引 firstIdnex-lastIndex之间的item可能部分在viewport中也可能都不在viewport中。 UIElementCollection children = this.InternalChildren;//因为配置了虚拟化,所以children的个数一直是viewport区域内的个数,如果没有虚拟化则是ItemSource的整个的个数 IItemContainerGenerator generator = this.ItemContainerGenerator; //获得第一个可被显示的item的位置 GeneratorPosition startPosi = generator.GeneratorPositionFromIndex(firstVisiableIndex); int childIndex = (startPosi.Offset == 0) ? startPosi.Index : startPosi.Index + 1;//startPosi在chilren中的索引 using (generator.StartAt(startPosi, GeneratorDirection.Forward, true)) { int itemIndex = firstVisiableIndex; while (itemIndex <= lastVisiableIndex)//生成lastVisiableIndex-firstVisiableIndex个item { bool newlyRealized = false; var child = generator.GenerateNext(out newlyRealized) as UIElement; if (newlyRealized) { if (childIndex >= children.Count) base.AddInternalChild(child); else { base.InsertInternalChild(childIndex, child); } generator.PrepareItemContainer(child); } else { //处理 正在显示的child被移除了这种情况 if (!child.Equals(children[childIndex])) { base.RemoveInternalChildRange(childIndex, 1); } } child.Measure(new Size(this.ChildWidth, this.ChildHeight)); //child.DesiredSize//child想要的size itemIndex++; childIndex++; } } CleanUpItems(firstVisiableIndex, lastVisiableIndex); return new Size(double.IsInfinity(availableSize.Width) ? 0 : availableSize.Width, double.IsInfinity(availableSize.Height) ? 0 : availableSize.Height);//自身想要的size } protected override Size ArrangeOverride(Size finalSize) { Debug.WriteLine("----ArrangeOverride"); var generator = this.ItemContainerGenerator; UpdateScrollInfo(finalSize); int childPerRow = CalculateChildrenPerRow(finalSize); double availableItemWidth = finalSize.Width / childPerRow; for (int i = 0; i <= this.Children.Count - 1; i++) { var child = this.Children[i]; int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0)); int row = itemIndex / childPerRow;//current row int column = itemIndex % childPerRow; double xCorrdForItem = 0; xCorrdForItem = column * availableItemWidth + (availableItemWidth - this.ChildWidth) / 2; Rect rec = new Rect(xCorrdForItem, row * this.ChildHeight, this.ChildWidth, this.ChildHeight); child.Arrange(rec); } return finalSize; } protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { base.OnRenderSizeChanged(sizeInfo); this.SetVerticalOffset(this.VerticalOffset); } protected override void OnClearChildren() { base.OnClearChildren(); this.SetVerticalOffset(0); } protected override void BringIndexIntoView(int index) { if (index < 0 || index >= Children.Count) throw new ArgumentOutOfRangeException(); int row = index / CalculateChildrenPerRow(RenderSize); SetVerticalOffset(row * this.ChildHeight); } #region IScrollInfo Interface public bool CanVerticallyScroll { get; set; } public bool CanHorizontallyScroll { get; set; } private Size extent = new Size(0, 0); public double ExtentWidth => this.extent.Width; public double ExtentHeight => this.extent.Height; private Size viewPort = new Size(0, 0); public double ViewportWidth => this.viewPort.Width; public double ViewportHeight => this.viewPort.Height; private Point offset; public double HorizontalOffset => this.offset.X; public double VerticalOffset => this.offset.Y; public ScrollViewer ScrollOwner { get; set; } public void LineDown() { this.SetVerticalOffset(this.VerticalOffset + this.ScrollOffset); } public void LineLeft() { throw new NotImplementedException(); } public void LineRight() { throw new NotImplementedException(); } public void LineUp() { this.SetVerticalOffset(this.VerticalOffset - this.ScrollOffset); } public Rect MakeVisible(Visual visual, Rect rectangle) { return new Rect(); } public void MouseWheelDown() { this.SetVerticalOffset(this.VerticalOffset + this.ScrollOffset); } public void MouseWheelLeft() { throw new NotImplementedException(); } public void MouseWheelRight() { throw new NotImplementedException(); } public void MouseWheelUp() { this.SetVerticalOffset(this.VerticalOffset - this.ScrollOffset); } public void PageDown() { this.SetVerticalOffset(this.VerticalOffset + this.viewPort.Height); } public void PageLeft() { throw new NotImplementedException(); } public void PageRight() { throw new NotImplementedException(); } public void PageUp() { this.SetVerticalOffset(this.VerticalOffset - this.viewPort.Height); } public void SetHorizontalOffset(double offset) { throw new NotImplementedException(); } public void SetVerticalOffset(double offset) { if (offset < 0 || this.viewPort.Height >= this.extent.Height) offset = 0; else if (offset + this.viewPort.Height >= this.extent.Height) offset = this.extent.Height - this.viewPort.Height; this.offset.Y = offset; this.ScrollOwner?.InvalidateScrollInfo(); this.trans.Y = -offset; this.InvalidateMeasure(); //接下来会触发MeasureOverride() } #endregion }
XML代码如下:
<ItemsControl Name="ItemsControllist" VirtualizingStackPanel.IsVirtualizing="True" Margin="10" VirtualizingStackPanel.CacheLength="100" VirtualizingStackPanel.CacheLengthUnit="Item" VirtualizingStackPanel.ScrollUnit="Pixel"> <ItemsControl.Template> <ControlTemplate> <Border Style="{StaticResource ResourceKey=BaseBorder}"> <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" CanContentScroll="True"> <ItemsPresenter VirtualizingStackPanel.VirtualizationMode="Standard"/> </ScrollViewer> </Border> </ControlTemplate> </ItemsControl.Template> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding }"/> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <local:VirtualizingWrapPanel ScrollOffset="50" ChildHeight="70" ChildWidth="70"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl>
标签:控件,虚拟化,int,void,availableSize,new,WPF,public From: https://www.cnblogs.com/lisghxfdfgh/p/18612983