ListView 在 VirtualMode 和复选框中






4.13/5 (10投票s)
当处理大量项目时,VirtualMode 模式下的 listview 控件是最快的方式。但是,对于包含复选框的项目应特别注意。
引言
加载具有大量项目的 ListView
控件是一个非常慢的过程。从 .NET 2.0 开始,ListView
控件包含一个 VirtualMode
。通过一些额外的代码,它可以显着加快此过程。但是,使用带复选框的项目时应特别小心。
背景
要使 ListView
在 VirtualMode
中工作,需要做两件事。设置 ListView
控件上的一些属性,并创建一个事件处理程序来“检索”虚拟项目。
void listView_RetrieveVirtualItem(object sender,
RetrieveVirtualItemEventArgs e)
{
// e contains ItemIndex
e.Item = listViewItem;
}
....
void SetupListview(bool blnVirtual)
{
...
this.listView1.VirtualMode = true;
this.listView1.RetrieveVirtualItem +=
new RetrieveVirtualItemEventHandler(listView_RetrieveVirtualItem);
...
}
使用附加的源代码
该代码展示了一个测试框架,您可以在 Normal
和 VirtualMode
之间切换。它测量加载 100000 (十万) 个 ListViewItem
所需的时间。
它使用静态缓存来简化 ListViewItem
。
private ListViewItem[] lvi;
...
private void Test()
{
// You can switch between Virtual and Normal
bool blnVirtual = true;
// Get the ListViewItem cache up and running
int NR = 100000;
lvi = new ListViewItem[NR];
for (int intI = 0; intI < lvi.Length; intI++)
lvi[intI] = new ListViewItem(intI + " test");
// Check some random items, just for testing
lvi[3].Checked = true;
lvi[5].Checked = true;
lvi[12].Checked = true;
lvi[NR-2].Checked = true;
...
}
.NET 2.0 有一个很好的 Stopwatch
,我用它来测量将项目从我们的缓存加载到 ListView
控件的性能。
...
Stopwatch stopwatch = new Stopwatch();
stopwatch.Reset();
stopwatch.Start();
SetupListview(blnVirtual);
stopwatch.Stop();
this.Text = "ListView VirtualMode=" + blnVirtual +
" : "+ lvi.Length + " items in " +
stopwatch.ElapsedMilliseconds + " mS";
...
为了测试 ListView
控件的正常性能,使用了以下代码
...
this.listView1 = new ListView();
this.listView1.Dock = DockStyle.Fill;
this.listView1.View = View.List;
this.listView1.CheckBoxes = true;
this.listView1.Items.AddRange(lvi);
...
结果,在 Normal
模式下加载 100000 个缓存项目花费了接近 35 秒的时间。
本文开头的图片显示了相同测试的时间,为 15 毫秒。 与控件的正常使用相比,VirtualMode
快了 2000 多倍。
关注点
当使用复选框和 VirtualMode
时,会出现一些问题。 加载项目时,只有选中的项目才具有可见的复选框。 其他的则没有。 此外,单击复选框本身不会产生任何结果。 双击一个项目只会显示一个选中的项目,只有当该项目失去焦点时才会显示。 这些是我发现的东西,可能还有更多已知的问题功能。
为了解决这个问题,我必须将 ListView
控件设置为 OwnerDrawing
,并在控件上挂钩一个单击和双击处理程序。
因为正常的 ListView
在绘制项目文本方面没有任何问题,所以设置了 DrawDefault
属性。 为了让未选中的项目也显示复选框,e.Item.Checked
属性在 true
和 false
之间切换以完成这项工作。
void listView_DrawItem(object sender, DrawListViewItemEventArgs e)
{
e.DrawDefault = true;
if (!e.Item.Checked)
{
e.Item.Checked = true;
e.Item.Checked = false;
}
}
要处理对复选框的单次点击,首先使用 GetItemAt
从鼠标点击的位置获取 ListViewItem
。 当找到有效的项目时,检查鼠标相对于项目的位置,以确定是否点击了复选框。 最后,使用 Invalidate
重新绘制该项目。
void listView_MouseClick(object sender, MouseEventArgs e)
{
ListView lv = (ListView)sender;
ListViewItem lvi = lv.GetItemAt(e.X, e.Y);
if (lvi != null)
{
if (e.X < (lvi.Bounds.Left + 16))
{
lvi.Checked = !lvi.Checked;
lv.Invalidate(lvi.Bounds);
}
}
}
因为双击项目有效,只有绘图受到影响,我们必须自己进行绘图。
void listView_MouseDoubleClick(object sender, MouseEventArgs e)
{
ListView lv = (ListView)sender;
ListViewItem lvi = lv.GetItemAt(e.X, e.Y);
if(lvi!=null)
lv.Invalidate(lvi.Bounds);
}
准备 ListView
控件以使用事件处理程序
...
// This makes it real fast!!
this.listView1.RetrieveVirtualItem +=
new RetrieveVirtualItemEventHandler(
listView_RetrieveVirtualItem);
this.listView1.VirtualListSize = lvi.Length;
this.listView1.VirtualMode = true;
// This is what you need, for drawing unchecked checkboxes
this.listView1.OwnerDraw = true;
this.listView1.DrawItem +=
new DrawListViewItemEventHandler(listView_DrawItem);
// Redraw when checked or doubleclicked
this.listView1.MouseClick +=
new MouseEventHandler(listView_MouseClick);
this.listView1.MouseDoubleClick +=
new MouseEventHandler(listView_MouseDoubleClick);
...
将所有这些绑定在一起,以使 ListView
在两种模式下都能工作。
void SetupListview(bool blnVirtual)
{
// Get ListView working
this.listView1 = new ListView();
this.listView1.Dock = DockStyle.Fill;
// This is what we want!!
this.listView1.View = View.List;
this.listView1.CheckBoxes = true;
if (blnVirtual)
{
// This makes it real fast!!
this.listView1.RetrieveVirtualItem +=
new RetrieveVirtualItemEventHandler(
listView_RetrieveVirtualItem);
this.listView1.VirtualListSize = lvi.Length;
this.listView1.VirtualMode = true;
// This is what you need, for drawing unchecked checkboxes
this.listView1.OwnerDraw = true;
this.listView1.DrawItem +=
new DrawListViewItemEventHandler(listView_DrawItem);
// Redraw when checked or doubleclicked
this.listView1.MouseClick +=
new MouseEventHandler(listView_MouseClick);
this.listView1.MouseDoubleClick +=
new MouseEventHandler(listView_MouseDoubleClick);
}
else
{
// The other way
this.listView1.Items.AddRange(lvi);
}
// Show in main form
this.Controls.Add(this.listView1);
}
历史
截至撰写本文时,它是 1.0 版本。