WPF中实现自定义虚拟容器(实现VirtualizingPanel)

在WPF应用程序开发过程中,大数据量的数据展现通常都要考虑性能问题。有下面一种常见的情况:原始数据源数据量很大,但是某一时刻数据容器中的可见元素个数是有限的,剩余大多数元素都处于不可见状态,如果一次性将所有的数据元素都渲染出来则会非常的消耗性能。因而可以考虑只渲染当前可视区域内的元素,当可视区域内的元素需要发生改变时,再渲染即将展现的元素,最后将不再需要展现的元素清除掉,这样可以大大提高性能。在WPF中System.Windows.Controls命名空间下的VirtualizingStackPanel可以实现数据展现的虚拟化功能,ListBox的默认元素展现容器就是它。但有时VirtualizingStackPanel的布局并不能满足我们的实际需要,此时就需要实现自定义布局的虚拟容器了。本文将简单介绍容器自定义布局,然后介绍实现虚拟容器的基本原理,最后给出一个虚拟化分页容器的演示程序。

一、WPF中自定义布局 (已了解容器自定义布局的朋友可略过此节)

通常实现一个自定义布局的容器,需要继承System.Windows.Controls.Panel, 并重写下面两个方法:

MeasureOverride —— 用来测量子元素期望的布局尺寸

ArrangeOverride —— 用来安排子元素在容器中的布局。

下面用一个简单的SplitPanel来加以说明这两个方法的作用。下面的Window中放置了一个SplitPanel,每点击一次“添加”按钮,都会向SplitPanel中添加一个填充了随机色的Rectangle, 而SplitPanel中的Rectangle无论有几个,都会在垂直方向上布满容器,水平方向上平均分配宽度。

实现代码如下:

SplitPanel
/// <summary>
/// 简单的自定义容器
/// 子元素在垂直方向布满容器,水平方向平局分配容器宽度
/// </summary>
public class SplitPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize); // 测量子元素期望布局尺寸(child.DesiredSize)
} return base.MeasureOverride(availableSize);
} protected override Size ArrangeOverride(Size finalSize)
{
if (double.IsInfinity(finalSize.Height) || double.IsInfinity(finalSize.Width))
{
throw new InvalidOperationException("容器的宽和高必须是确定值");
} if (Children.Count > )
{
double childAverageWidth = finalSize.Width / Children.Count;
for (int childIndex = ; childIndex < InternalChildren.Count; childIndex++)
{
// 计算子元素将被安排的布局区域
var rect = new Rect(childIndex * childAverageWidth, , childAverageWidth, finalSize.Height);
InternalChildren[childIndex].Arrange(rect);
}
} return base.ArrangeOverride(finalSize);
}
}

SplitPanel

SplitPanel的MeasureOverride 方法参数availableSize是容器可以给出的总布局大小,在方法体中只依次调用了子元素的Measure方法,调用该方法后,子元素的DesiredSize属性就会被赋值, 该属性指明了子元素期望的布局尺寸。(在SplitPanel中并不需要知道子元素的期望布局尺寸,所以可以不必重写MeasureOverride 方法,但是在一些比较复杂的布局中需要用到子元素的DesiredSize属性时就必须重写)

SplitPaneld的ArrangeOverride 方法参数finalSize是容器最终给出的布局大小,26行根据子元素个数先计算出子元素平均宽度,30行再按照子元素索引计算出各自的布局区域信息。然后31行调用子元素的Arrange方法将子元素安排在容器中的合适位置。这样就可以实现期望的布局效果。当UI重绘时(例如子元素个数发生改变、容器布局尺寸发生改变、强制刷新UI等),会重新执行MeasureOverride 和ArrangeOverride 方法。

二、虚拟容器原理

要想实现一个虚拟容器,并让虚拟容器正常工作,必须满足以下两个条件:

1、容器继承自System.Windows.Controls.VirtualizingPanel,并实现子元素的实例化、虚拟化及布局处理。

2、虚拟容器要做为一个System.Windows.Controls.ItemsControl(或继承自ItemsControl的类)实例的ItemsPanel(实际上是定义一个ItemsPanelTemplate) 

下面我们先来了解一下ItemsControl的工作机制:

当我们为一个ItemsControl指定了ItemsSource属性后,ItemsControl的Items属性就会被初始化,这里面装的就是原始的数据(题外话:通过修改Items的Filter可以实现不切换数据源的元素过滤,修改Items的SortDescriptions属性可以实现不切换数据源的元素排序)。之后ItemsControl会根据Items来生成子元素的容器(ItemsControl生成ContentPresenter, ListBox生成ListBoxItem, ComboBox生成ComboBox等等),同时将子元素容器的DataContext设置为与之对应的数据源,最后每个子元素容器再根据ItemTemplate的定义来渲染子元素实际显示效果。

对于Panel来说,ItemsControl会一次性生成所有子元素的子元素容器并进行数据初始化,这样就导致在数据量较大时性能会很差。而对于VirtualizingPanel,ItemsControl则不会自动生成子元素容器及子元素的渲染,这一过程需要编程实现。

接下来我们引入另一个重要概念:GeneratorPosition,这个结构体用来描述ItemsControl的Items属性中实例化和虚拟化数据项的位置关系,在VirtualizingPanel中可以通过ItemContainerGenerator(注意:在VirtualizingPanel第一次访问这个属性之前要先访问一下InternalChildren属性,否则ItemContainerGenerator会是null,貌似是一个Bug)属性来获取数据项的位置信息,此外通过这个属性还可以进行数据项的实例化和虚拟化。

获取数据项GeneratorPosition信息:

DumpGeneratorContent
/// <summary>
/// 显示数据GeneratorPosition信息
/// </summary>
public void DumpGeneratorContent()
{
IItemContainerGenerator generator = this.ItemContainerGenerator;
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this); Console.WriteLine("Generator positions:");
for (int i = ; i < itemsControl.Items.Count; i++)
{
GeneratorPosition position = generator.GeneratorPositionFromIndex(i);
Console.WriteLine("Item index=" + i + ", Generator position: index=" + position.Index + ", offset=" + position.Offset);
}
Console.WriteLine();
}

DumpGeneratorContent

第7行通过ItemsControl的静态方法GetItemsOwner可以找到容器所在的ItemsControl,这样就可以访问到数据项集合,第12行代码调用generator 的GeneratorPositionFromIndex方法,通过数据项的索引得到数据项的GeneratorPosition 信息。

 

数据项实例化:

 
/// <summary>
/// 实例化子元素
/// </summary>
/// <param name="itemIndex">数据条目索引</param>
public void RealizeChild(int itemIndex)
{
IItemContainerGenerator generator = this.ItemContainerGenerator;
GeneratorPosition position = generator.GeneratorPositionFromIndex(itemIndex); using (generator.StartAt(position, GeneratorDirection.Forward, allowStartAtRealizedItem: true))
{
bool isNewlyRealized;
var child = (UIElement)generator.GenerateNext(out isNewlyRealized); // 实例化(构造出空的子元素UI容器) if (isNewlyRealized)
{
generator.PrepareItemContainer(child); // 填充UI容器数据
}
}
}

第10行调用generator 的StartAt方法确定准备实例化元素的数据项位置,第13行调用generator的GenerateNext方法进行数据项的实例化,输出参数isNewlyRealized为ture则表明该元素是从虚拟化状态实例化出来的,false则表明该元素已被实例化。注意,该方法只是构造出了子元素的UI容器,只有调用了17行的PrepareItemContainer方法,UI容器的实际内容才会根据ItemsControl的ItemTemplate定义进行渲染。

 

数据项虚拟化:

 
VirtualizeChild
/// <summary>
/// 虚拟化子元素
/// </summary>
/// <param name="itemIndex">数据条目索引</param>
public void VirtualizeChild(int itemIndex)
{
IItemContainerGenerator generator = this.ItemContainerGenerator;
var childGeneratorPos = generator.GeneratorPositionFromIndex(itemIndex);
if (childGeneratorPos.Offset == )
{
generator.Remove(childGeneratorPos, ); // 虚拟化(从子元素UI容器中清除数据)
}
}

VirtualizeChild

通过数据条目索引得出GeneratorPosition 信息,之后在11行调用generator的Remove方法即可实现元素的虚拟化。

 
通过几张图片来有一个直观的认识,数据条目一共有10个,初始化时全部都为虚拟化状态:

 

实例化第二个元素:

 

增加实例化第三、七个元素:

 

虚拟化第二个元素:

通过观察可以发现,实例化的数据项位置信息按顺序从0开始依次增加,所有实例化的数据项位置信息的offset属性都是0,虚拟化数据项index和前一个最近的实例化元素index保持一致,offset依次增加

 

三、实战-实现一个虚拟化分页容器

了解了子元素自定义布局、数据项GeneratorPosition信息、虚拟化、实例化相关概念和实现方法后,离实现一个自定义虚拟容器还剩一步重要的工作:计算当前应该显示的数据项起止索引,实例化这些数据项,虚拟化不再显示的数据项。

再前进一步,实现一个虚拟化分页容器:

这个虚拟化分页容器有ChildWidth和ChildHeight两个依赖属性,用来定义容器中子元素的宽和高,这样在容器布局尺寸确定的情况下可以计算出可用布局下一共能显示多少个子元素,也就是PageSize属性。为容器指定一个有5000个数据的数据源,再提供一个分页控件用来控制分页容器的PageIndex,用来达到分页显示的效果。

贴出主要代码:

计算需要实例化数据项的起止索引
/// <summary>
/// 计算可是元素起止索引
/// </summary>
/// <param name="availableSize">可用布局尺寸</param>
/// <param name="firstVisibleChildIndex">第一个显示的子元素索引</param>
/// <param name="lastVisibleChildIndex">最后一个显示的子元素索引</param>
private void ComputeVisibleChildIndex(Size availableSize, out int firstVisibleChildIndex, out int lastVisibleChildIndex)
{
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this); if (itemsControl != null && itemsControl.Items != null && ChildWidth > && ChildHeight > )
{
ChildrenCount = itemsControl.Items.Count; _horizontalChildMaxCount = (int)(availableSize.Width / ChildWidth);
_verticalChildMaxCount = (int)(availableSize.Height / ChildHeight); PageSize = _horizontalChildMaxCount * _verticalChildMaxCount; // 计算子元素显示起止索引
firstVisibleChildIndex = PageIndex * PageSize;
lastVisibleChildIndex = Math.Min(ChildrenCount, firstVisibleChildIndex + PageSize) - ; Debug.WriteLine("firstVisibleChildIndex:{0}, lastVisibleChildIndex{1}", firstVisibleChildIndex, lastVisibleChildIndex)
}
else
{
ChildrenCount = ;
firstVisibleChildIndex = -;
lastVisibleChildIndex = -;
PageSize = ;
}
}

计算需要实例化数据项的起止索引

测量子元素布局期望尺寸及数据项实例化
/// <summary>
/// 测量子元素布局,生成需要显示的子元素
/// </summary>
/// <param name="availableSize">可用布局尺寸</param>
/// <param name="firstVisibleChildIndex">第一个显示的子元素索引</param>
/// <param name="lastVisibleChildIndex">最后一个显示的子元素索引</param>
private void MeasureChild(Size availableSize, int firstVisibleChildIndex, int lastVisibleChildIndex)
{
if (firstVisibleChildIndex < )
{
return;
} // 注意,在第一次使用 ItemContainerGenerator之前要先访问一下InternalChildren,
// 否则ItemContainerGenerator为null,是一个Bug
UIElementCollection children = InternalChildren;
IItemContainerGenerator generator = ItemContainerGenerator; // 获取第一个可视元素位置信息
GeneratorPosition position = generator.GeneratorPositionFromIndex(firstVisibleChildIndex);
// 根据元素位置信息计算子元素索引
int childIndex = position.Offset == ? position.Index : position.Index + ; using (generator.StartAt(position, GeneratorDirection.Forward, true))
{
for (int itemIndex = firstVisibleChildIndex; itemIndex <= lastVisibleChildIndex; itemIndex++, childIndex++)
{
bool isNewlyRealized; // 用以指示新生成的元素是否是新实体化的 // 生成下一个子元素
var child = (UIElement)generator.GenerateNext(out isNewlyRealized); if (isNewlyRealized)
{
if (childIndex >= children.Count)
{
AddInternalChild(child);
}
else
{
InsertInternalChild(childIndex, child);
}
generator.PrepareItemContainer(child);
} // 测算子元素布局
child.Measure(availableSize);
}
}
}
清理不再显示的子元素
/// <summary>
/// 清理不需要显示的子元素
/// </summary>
/// <param name="firstVisibleChildIndex">第一个显示的子元素索引</param>
/// <param name="lastVisibleChildIndex">最后一个显示的子元素索引</param>
private void CleanUpItems(int firstVisibleChildIndex, int lastVisibleChildIndex)
{
UIElementCollection children = this.InternalChildren;
IItemContainerGenerator generator = this.ItemContainerGenerator; // 清除不需要显示的子元素,注意从集合后向前操作,以免造成操作过程中元素索引发生改变
for (int i = children.Count - ; i > -; i--)
{
// 通过已显示的子元素的位置信息得出元素索引
var childGeneratorPos = new GeneratorPosition(i, );
int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos); // 移除不再显示的元素
if (itemIndex < firstVisibleChildIndex || itemIndex > lastVisibleChildIndex)
{
generator.Remove(childGeneratorPos, );
RemoveInternalChildRange(i, );
}
}
}

清理不再显示的子元素

本文章摘自: http://www.cnblogs.com/talywy/archive/2012/09/07/CustomVirtualizingPanel.html  非常棒,非常感谢博主

WPF中实现自定义虚拟容器(实现VirtualizingPanel)的更多相关文章

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

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

  2. C#、WPF中如何自定义鼠标样式

    需求:在C#中如何自定义鼠标样式?在这里可以分两种情况,一种是在winForm,另一种是在WPF中(注意使用的Cursor对象不一样) 解决办法如下: a.首先针对WinForm中,我们可以采用图标加 ...

  3. WPF中DATAGRID自定义验证(包含BINDINGGROUP)

    DataGrid在Wpf中的应用是十分广泛的,当你需要表中的信息稍详细的显示出来时,或者我们需要进行某些数据输入时,都有可能采用DataGrid.当然对信息的显示,我们不需要进行验证,但当我们将Dat ...

  4. Silverlight及WPF中实现自定义BusyIndicator

    在开发Silverlight或者WPF项目时,当我们调用Web服务来加载一些数据时,由于数据量比较大需要较长的时间,需要用户等待,为了给用户友好的提示和避免用户在加载数据过程中进行重复操作,我们通常使 ...

  5. WPF中DataGrid自定义实现最后一行下面跟一个汇总行,类似MT4

    1.先看MT4实现的效果:(图中红框部分),其实就是DataGrid在最后一行下面跟一个汇总的显示条 2.看我WPF实现的效果,汇总行中的数据可以绑定哦!效果图如下: 我扩展了一下DataGrid控件 ...

  6. WPF中的导航框架(一)——概述

    有的时候,我们需要一个支持页面跳转的UI,例如文件浏览器,开始向导等.对于这样的界面,简单的可以使用ContentControl + ContentTemplateSelector的方式来实现,但是有 ...

  7. [转]WPF中的导航框架

    有的时候,我们需要一个支持页面跳转的UI,例如文件浏览器,开始向导等.对于这样的界面,简单的可以使用ContentControl + ContentTemplateSelector的方式来实现,但是有 ...

  8. WPF中自定义绘制内容

    先说结论:实现了在自定义大小的窗口中,加载图片,并在图片上绘制一个矩形框:且在窗口大小改变的情况,保持绘制的矩形框与图片的先对位置不变. 在WinForm中,我们可以很方便地绘制自己需要的内容,在WP ...

  9. WPF 中,动态创建Button,并使Button得样式按照自定义的Resource样式显示

    第一步:自定义一个Button的样式 1.新建一个xaml文件,在其中自定义好自己的Resources 这个Resource 的根节点是 <ResourceDictionary xmlns=&q ...

随机推荐

  1. 2.C#中通过委托Func消除重复代码

    阅读目录   一:重复的代码 二:C#中通过委托Func消除重复代码 一:重复代码    public class Persion { public string Name { get; set; } ...

  2. SQL SERVER 移动系统数据库

    移动系统数据库在下列情况下可能很有用: 故障恢复.例如,数据库处于可疑模式下或因硬件故障而关闭. 计划的重定位. 为预定的磁盘维护操作而进行的重定位. 移动 Master 数据库     在“开始”菜 ...

  3. Openvswitch原理与代码分析(3): openvswitch内核模块的加载

      上一节我们讲了ovs-vswitchd,其中虚拟网桥初始化的时候,对调用内核模块来添加虚拟网卡.   我们从openvswitch内核模块的加载过程,来看这个过程.   在datapath/dat ...

  4. cmd实用命令

    1.netstat 查看电脑端口状况 实际应用举例:查看某软件坚监听的电脑端口. 在任务管理器中选择列...,打开PID的显示.在这里查看某个应用程序的线程ID是多少.例如QQ:4904. 运行,cm ...

  5. iOS工程集成支付宝错误Undefined symbols for architecture armv7

    问题描述: 新工程中需要集成支付宝功能,于是咱就把支付宝的库给集成了进入然后就出现了下面这种错误了说,错误信息如下: Undefined symbols for architecture armv7: ...

  6. Linux多线程编程(不限Linux)【转】

    ——本文一个例子展开,介绍Linux下面线程的操作.多线程的同步和互斥. 前言 线程?为什么有了进程还需要线程呢,他们有什么区别?使用线程有什么优势呢?还有多线程编程的一些细节问题,如线程之间怎样同步 ...

  7. WordPress博客搬家注意事项

    博客域名还有一段时间就到期了,准备更换域名和空间,会出现一些问题,我这里在网上收集整理了一下,基本上会遇到两个重要的问题. 首先第一个问题就是数据的搬迁中出现的错误. 我这里用的是phpmyadmin ...

  8. java之接口interface

    接口 1.多个无关的类可以实现同一个接口 2.一个类可以实现多个无关的接口 3.与继承关系类似,接口与实现类之间存在多态性 4.定义java类的语法格式 < modifier> class ...

  9. easyui filebox 浏览图片

    <img id="image1"/> <input id="f1" class="easyui-filebox" name ...

  10. Android Matirx的简介

    在Android中,对图片的处理需要使用到Matrix类,Matrix是一个3 x 3的矩阵,他对图片的处理分为四个基本类型: 1.Translate————平移X,Y轴变换,而不是移动图形 2.Sc ...