WPF之路——实现自定义虚拟容器(实现VirtualizingPanel)
原文:WPF之路——实现自定义虚拟容器(实现VirtualizingPanel)
源码下载地址:
http://download.csdn.net/detail/qianshen88/6618033
在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 > 0)
{
double childAverageWidth = finalSize.Width / Children.Count;
for (int childIndex = 0; childIndex < InternalChildren.Count; childIndex++)
{
// 计算子元素将被安排的布局区域
var rect = new Rect(childIndex * childAverageWidth, 0, childAverageWidth, finalSize.Height);
InternalChildren[childIndex].Arrange(rect);
}
} return base.ArrangeOverride(finalSize);
}
}
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 = 0; 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();
}
第7行通过ItemsControl的静态方法GetItemsOwner可以找到容器所在的ItemsControl,这样就可以访问到数据项集合,第12行代码调用generator 的GeneratorPositionFromIndex方法,通过数据项的索引得到数据项的GeneratorPosition 信息。
数据项实例化:
RealizeChild
/// <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 == 0)
{
generator.Remove(childGeneratorPos, 1); // 虚拟化(从子元素UI容器中清除数据)
}
}
通过数据条目索引得出GeneratorPosition 信息,之后在11行调用generator的Remove方法即可实现元素的虚拟化。
实例化第二个元素:
增加实例化第三、七个元素:
虚拟化第二个元素:
通过观察可以发现,实例化的数据项位置信息按顺序从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 > 0 && ChildHeight > 0)
{
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) - 1; Debug.WriteLine("firstVisibleChildIndex:{0}, lastVisibleChildIndex{1}", firstVisibleChildIndex, lastVisibleChildIndex)
}
else
{
ChildrenCount = 0;
firstVisibleChildIndex = -1;
lastVisibleChildIndex = -1;
PageSize = 0;
}
}
测量子元素布局期望尺寸及数据项实例化
/// <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 < 0)
{
return;
} // 注意,在第一次使用 ItemContainerGenerator之前要先访问一下InternalChildren,
// 否则ItemContainerGenerator为null,是一个Bug
UIElementCollection children = InternalChildren;
IItemContainerGenerator generator = ItemContainerGenerator; // 获取第一个可视元素位置信息
GeneratorPosition position = generator.GeneratorPositionFromIndex(firstVisibleChildIndex);
// 根据元素位置信息计算子元素索引
int childIndex = position.Offset == 0 ? position.Index : position.Index + 1; 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 - 1; i > -1; i--)
{
// 通过已显示的子元素的位置信息得出元素索引
var childGeneratorPos = new GeneratorPosition(i, 0);
int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos); // 移除不再显示的元素
if (itemIndex < firstVisibleChildIndex || itemIndex > lastVisibleChildIndex)
{
generator.Remove(childGeneratorPos, 1);
RemoveInternalChildRange(i, 1);
}
}
}
WPF之路——实现自定义虚拟容器(实现VirtualizingPanel)的更多相关文章
- WPF中实现自定义虚拟容器(实现VirtualizingPanel)
WPF中实现自定义虚拟容器(实现VirtualizingPanel) 在WPF应用程序开发过程中,大数据量的数据展现通常都要考虑性能问题.有下面一种常见的情况:原始数据源数据量很大,但是某一时刻数据容 ...
- WPF之路-键盘与鼠标事件 - 简书
原文:WPF之路-键盘与鼠标事件 - 简书 键盘事件 事件类型分为以下几个类型 生命周期事件:在元素加载与卸载的时候发生 鼠标事件:鼠标动作 键盘事件:键盘动作 手写笔事件:适用于win7以上的系统 ...
- 异数OS-织梦师-异数OS虚拟容器交换机(七) 走进4Tbps网络应用时代,加速5G应用真正落地
. 异数OS-织梦师-异数OS虚拟容器交换机(七) 走进4Tbps网络应用时代,加速5G应用真正落地 本文来自异数OS社区 github: https://github.com/yds086/Here ...
- Android开发案例 - 自定义虚拟键盘
所有包含IM功能的App(如微信, 微博, QQ, 支付宝等)都提供了Emoji表情之类的虚拟键盘, 如下图: 本文只着重介绍如何实现输入法键盘和自定义虚拟键盘的流畅切换, 而不介绍如何实现虚 ...
- 自定义Docker容器的 hostname
自定义Docker容器的 hostname 作者: John Deng 原创内容,欢迎传播,请注明出处:http://www.cnblogs.com/johnd/p/set-docker-host ...
- WPF Step By Step 自定义模板
WPF Step By Step 自定义模板 回顾 上一篇,我们简单介绍了几个基本的控件,本节我们将讲解每个控件的样式的自定义和数据模板的自定义,我们会结合项目中的具体的要求和场景来分析,给出我们实现 ...
- 关于在android手机中腾讯、阿里产品不自定义虚拟键盘的想法
1,自定义虚拟键盘,影响用户体验.你每个用户的喜好不一样,都有自己心仪的一款输入法.腾讯或是阿里设计出来的输入法很难满足上亿用户的喜好,到时候又是一场口水战,再说了就是专业的输入法肯定要比应用里嵌套的 ...
- 云计算之路-阿里云上-容器难容:容器服务故障以及自建 docker swarm 集群故障
3月21日,由于使用阿里云服务器自建 docker swarm 集群的不稳定,我们将自建 docker swarm 集群上的所有应用切换阿里云容器服务 swarm 版(非swarm mode). 3月 ...
- wpf自定义控件中使用自定义事件
wpf自定义控件中使用自定义事件 1 创建自定义控件及自定义事件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 2 ...
随机推荐
- 【u002】数列排序(seqsort)
Time Limit: 1 second Memory Limit: 128 MB [问题描述] 给定一个数列{an},这个数列满足ai≠aj(i≠j),现在要求你把这个数列从小到大排序,每次允许你交 ...
- android使用Gson来解析json
Gson是一种对象的解析json,非常好用,介绍一个站点http://json.parser.online.fr/能够帮我们看一个字符串是不是Json 对于Json文件 { "id" ...
- 主从同步设置的重要参数log_slave_updates
说明:最近部署了mysql的集群环境,详细如下M01和M02为主主复制,M01和R01为主从复制:在测试的过程中发现了以下问题: 1.M01和M02的主主复制是没有问题的(从M01写入数据能同步到M0 ...
- iOS中js与objective-c的简单交互
1.首先是objective-c调用js中的代码,可以用UIWebview中的一个方法 stringByEvaluatingJavaScriptFromString:后面接的是js中的方法名.这个函数 ...
- WPF Chart 图标
DevExpress: <dxc:ChartControl.Diagram> <dxc:XYDiagram2D.SeriesTemplate> </dxc:XYDiagr ...
- try catch finally中的return
try catch 中finally语句总是可以执行的,不管try中是否含有return语句 public class TestReturn { public static void main(Str ...
- ScrollView嵌套ListView问题
ScrollView嵌套ListView问题 导致Listview 第一主角 它是 Listview 的item 显示器的数量是不完全. 这是因为item 中间 Textview 话是太多的问题 一 ...
- BZOJ 1509 逃学的小孩 - 树型dp
传送门 题目大意: 在一棵树中, 每条边都有一个长度值, 现要求在树中选择 3 个点 X.Y. Z , 满足 X 到 Y 的距离不大于 X 到 Z 的距离, 且 X 到 Y 的距离与 Y 到 Z 的距 ...
- 无法写入预编译头文件,由于 IO 设备错误,无法运行此项请求的错误的解决
作者:朱金灿 来源:http://blog.csdn.net/clever101 早上查看服务器每日构建的情况,发现出现一个诡异的错误: fatal error C1085: 无法写入预编译头文件:& ...
- Android中SQLite数据库操作(1)——使用SQL语句操作SQLite数据库
下面是最原始的方法,用SQL语句操作数据库.后面的"Android中SQLite数据库操作(2)--SQLiteOpenHelper类"将介绍一种常用的android封装操作SQL ...