在 .NET 中实现无闪烁 ListView - 第 2 部分






4.50/5 (16投票s)
2003年2月4日
6分钟阅读

246895

2448
本文讨论了两种减少 .NET ListView 闪烁的方法。
引言
这是另一篇关于减少 ListView 闪烁的文章。我最初写的文章只适用于 Windows XP(需要清单文件来使用 Common Controls 版本 6)。您可以通过点击此链接查看:ListViewXP
本文将重点介绍另外两种技术。这两种技术都不需要 Windows XP。请注意,本文的重点是在不闪烁的情况下,使用尺寸合适的图像列表动态添加多个项目。
WndProc 技术
第一种技术涉及捕获和操作消息。正如我在上一篇文章中提到的,每次向 ListView
添加项目时,整个控件都会失效。在循环中添加多个项目时,除非您在循环结束时添加 Update()
或 Refresh()
(或其它各种方法),否则控件只会在循环结束时绘制。使用 Update()
、Refresh()
等会导致闪烁,因为控件会不断失效并重新绘制自身。观察这种情况的一种方法是重写 WndProc
方法,并将所有消息存储在 ArrayList
或其他结构中。然后当您查看 ArrayList
时,您会看到与 WM_ERASEBKGND
和 WM_PAINT
对应的数字。我们可以利用这些知识来帮助我们。
我们这个新类设计的一部分是允许程序员指定他/她正在添加新项目。我们创建了一个名为 UpdateItem(int iIndex)
的新方法。程序员可以调用此方法,它将只绘制新创建的项目。
public void UpdateItem(int iIndex)
{
updating = true;
itemnumber = iIndex;
this.Update();
updating = false;
}
首先,我们有一个名为 bool
的私有成员变量 updating。这将在下面的 WndProc
方法中使用。当它设置为 true 时,WndProc
方法将捕获消息。当 false
时,WndProc
除了调用 base.WndProc()
外,不执行任何操作。itemnumber
是另一个新添加的私有成员。它保存新添加项目的索引。Update()
用于重新绘制控件中失效的区域(我们在 WndProc
中操作的),最后它将 updating 设置为 false,以便我们的代码不再捕获消息。现在我们重写 WndProc
以进行调整。
protected override void WndProc(ref Message messg)
{
if (updating)
{
// We do not want to erase the background,
// turn this message into a null-message
if ((int)WM.WM_ERASEBKGND == messg.Msg)
messg.Msg = (int) WM.WM_NULL;
else if ((int)WM.WM_PAINT == messg.Msg)
{
RECT vrect = this.GetWindowRECT();
// validate the entire window
ValidateRect(this.Handle, ref vrect);
//Invalidate only the new item
Invalidate(this.Items[itemnumber].Bounds);
}
}
base.WndProc(ref messg);
}
如您所见,我们捕获了 WM_ERASEBKGND
消息,并将其转换为 NULL
消息。这将阻止 ListView
被擦除。WM_PAINT
用于重新绘制失效区域。由于整个控件都失效了,我们需要做一些工作。首先,我们使整个可见区域 有效。这样做会导致 WM_PAINT
不执行任何操作,因此我们紧接着只使新项目的边界失效。现在,当发生绘制时,它将只针对新项目发生,因为控件的其余部分已经有效。
WM_ERASEBKGND
和 WM_PAINT
是枚举类型,表示窗口消息的 int
值。您可以在代码中查看这些内容。ValidateRect
是一个 Win32 函数,允许我们验证某个区域。由于 .NET 中没有等效项,我们从 user32.dll 导入此函数(请参阅代码)。RECT
是一个类似于 Rectangle
的结构,但 ValidateRect
函数需要它。当我们调用 ValidateRect
时,它会验证 ListView
的整个窗口区域。然后我们使用控件的 Invalidate 方法使新项目的边界失效。现在只有新项目的边界失效。这意味着只会绘制新项目。由于我们捕获了 WM_ERASEBKGND
并验证了控件的其余部分,因此每个项目在添加时都会被绘制(前提是您使用了新添加的 UpdateItem(index)
函数)。如果您不使用 UpdateItem()
,WndProc
将不执行任何操作,因为“updating”变量将始终为 false。
注意 - 我想感谢 Carlos H Perez。他在他的 ListViewEx
控件中做了类似的事情,这个想法的一部分是基于那项工作。
现在我们可以在主应用程序中这样做
for (int i=0; i < 500; i++)
{
ListViewItem lvi =
new ListViewItem("Item #" + i.ToString(), indexOfImage);
listViewFF.Items.Add(lvi);
listViewFF.UpdateItem(i);
}
现在,项目将在添加到列表视图时被绘制,而不会闪烁。
纯 .NET 技术
下一项技术是纯 .NET,您不需要扩展 ListView
类。这项技术不会在项目添加时绘制它们,但描述了一种替代方法来防止用户长时间等待。
如果我们不将 ImageIndex
与 ListView 项目关联(将 ImageIndex
用作 -1),我们可以非常快速地将项目添加到 ListView
for (int i=0; i < 500; i++)
{
ListViewItem lvi =
new ListViewItem("Item #" + i.ToString(), -1);
this.listView.Items.Add(lvi);
}
由于我们没有调用 Update()
或 Refresh()
,我们不会在项目添加时看到它们,但这会很快完成,因为没有图像与 ListView
关联。
现在所有项目都已添加,因此我们需要将图像与它们关联。
for (int i=0; i < 500; i++)
{
ListViewItem lvi = this.listView.Items[i] ;
// get the already existing item
lvi.ImageIndex = someNumber ;
// Now we associate the item with an image in the imagelist;
this.listView.Invalidate(lvi.Bounds);
// Invalidate the already-existing item
this.Update();
// Will force the invalidated region to be redrawn
}
在循环开始之前,所有项目都将存在,没有图像。现在我们只需关联图像,并使项目的区域失效。调用 Update()
将只重新绘制失效区域。在这种情况下,只有项目的边界将失效,因此图像将绘制到屏幕上,而不会影响其他 ListView
项目。
这对于缩略图程序很有用。您可以在第一个循环中添加缩略图的名称,在第二个循环中,您可以将位图加载到图像列表中,将已创建的项目与新添加图像的图像索引关联,现在每个图像都将在加载时被绘制。更好的是,您可以将这个第二个循环放入一个线程中。这将允许用户在绘制图像时滚动列表视图。
技术的优缺点
您应该根据需要使用每种技术。WndProc
技术允许您在项目添加时看到它们。如果您添加数千个项目,整体可能会更慢,但至少用户可以看到正在发生的事情。如果第一个循环中尝试添加(比如说)5000个项目,纯 .NET 技术将导致列表视图空白几秒钟,但如果第二个循环在线程中,则在绘制图像时不会阻止用户做其他事情。
关于 WndProc
技术有一点需要注意。由于某种原因,如果您使用清单文件来使用新的 XP 主题,它不能很好地工作。我不确定为什么,但有些项目在绘制其他项目时被擦除了。我确实找到了一个解决方案(它用于源代码中)。在循环开始之前,将列表视图的 AutoArrange = false
。当循环结束时,您可以将其设置回 true
。当然,如果您只使用 Windows XP,您可以使用我在上一篇文章中描述的双缓冲技术(请参阅介绍中的链接)。如果您不打算支持 XP 主题,则不需要更改 AutoArrange
属性。
关于编译源代码的说明
您需要 MS Visual Studio .NET 来查看此项目。解压所有文件后,将有两个目录:ListViewX 和 TestListViewFF。打开 ListViewX 文件夹中的解决方案文件,加载后,将 TestListViewFF 项目设置为启动项目。然后编译应该没有问题。