双缓冲的树形和列表视图






4.94/5 (21投票s)
实现原生 WinForms 无闪烁的 TreeView 和 ListView 后代
引言
在本文中,我将描述实现无闪烁的原生 WinForms TreeView
和 ListView
派生类。该解决方案将分几个迭代来描述,这些迭代与实际开发时间相关。所有这些都是为我的免费软件 文件管理器 Nomad.NET 创建并使用的。
问题
如果您曾经使用过默认的 WinForms TreeView
或 ListView
控件,您会注意到当控件大小改变时(例如,用鼠标改变窗体大小时)会发生严重的闪烁。在本文中,我将尝试找到解决此行为的方法,并通过使用双缓冲来消除闪烁。当我第一次注意到这个问题时,我做了一些谷歌搜索,但没有找到任何解决方案,所以我自己发明了几种解决方案。
第一次尝试 TreeView
首先,让我解释一下问题发生的原因。通常,控件的绘制周期包括两个阶段:擦除和绘制。首先,Windows 发送一个擦除背景的消息,然后是绘制消息。当控件收到擦除消息时,如果用纯色填充控件表面,则控件会在表面上绘制内容。当用户改变窗体大小时,Windows 会调用许多绘制周期,每个周期都会发生擦除和绘制,但由于绘制发生在擦除之后才发生,用户会看到明显的闪烁。
所以我的第一个想法是捕获擦除消息,并通过使用剪裁区域将 treeview
标签排除在绘制周期之外。
为此,我重写了默认的 WndProc
方法,并捕获了 WM_ERASEBKGND
和 WM_PAINT
消息。这个解决方案对我来说效果很好,持续了一年左右,但有些部分仍然闪烁(树线、按钮和图标),有时(很少)选定的节点会以瑕疵绘制。
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_ERASEBKGND:
m.Result = (IntPtr)1;
return;
case WM_PAINT:
Region BkgndRegion = null;
IntPtr RgnHandle = CreateRectRgn(0, 0, 0, 0);
try
{
RegionResult Result = GetUpdateRgn(Handle, RgnHandle, false);
if ((Result == RegionResult.SIMPLEREGION) ||
(Result == RegionResult.COMPLEXREGION))
BkgndRegion = Region.FromHrgn(RgnHandle);
}
finally
{
DeleteObject(RgnHandle);
}
using (BkgndRegion)
{
if ((BkgndRegion != null) && BkgndRegion.IsVisible(ClientRectangle))
{
int I = 0;
TreeNode Node = TopNode;
while ((Node != null) && (I++ <= VisibleCount))
{
BkgndRegion.Exclude(Node.Bounds);
Node = Node.NextVisibleNode;
}
if (BkgndRegion.IsVisible(ClientRectangle))
{
using (Brush BackBrush = new SolidBrush(BackColor))
using (Graphics Canvas = Graphics.FromHwnd(Handle))
Canvas.FillRegion(BackBrush, BkgndRegion);
}
}
}
break;
}
base.WndProc(ref m);
}
第二次尝试
当我开始将我的项目迁移到 Windows Vista 时,我发现 Microsoft 实现了原生双缓冲,我的任务只是启用这种功能。为此,我会在控件窗口句柄创建后发送 TVM_SETEXTENDEDSTYLE
消息,并带上 TVS_EX_DOUBLEBUFFER
样式。这效果非常好,但只在 Windows Vista 下。
public DbTreeView()
{
// Enable default double buffering processing (DoubleBuffered returns true)
SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint, true);
}
private void UpdateExtendedStyles()
{
int Style = 0;
if (DoubleBuffered)
Style |= TVS_EX_DOUBLEBUFFER;
if (Style != 0)
SendMessage(Handle, TVM_SETEXTENDEDSTYLE,
(IntPtr)TVS_EX_DOUBLEBUFFER, (IntPtr)Style);
}
protected override OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
UpdateExtendedStyles();
}
第三次尝试
在 Vista 下实现了原生双缓冲后,我开始考虑旧系统,特别是 Windows XP,因为它拥有庞大的用户群。所以我回到了我的第一次尝试并开始进行研究。首先,我尝试通过重写 NM_CUSTOMDRAW
消息来替换 owner draw hdc,但没有成功(似乎 Windows 没有检测到句柄替换并忽略了它)。
然后我想起了 WM_PRINT
消息。这个消息用于在用户提供的表面上绘制控件,通常用于打印。但为什么不使用这个消息将控件绘制到位图上,然后在绘制周期中绘制这个位图呢?
第一个版本使用了背景位图进行离屏绘制,而且奏效了!在证明了概念后,我开始了优化过程,并将位图替换为 .NET 的 BufferedGraphics
类。这个类是专门为在控件中实现双缓冲而创建的。
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
if (bg != null)
{
bg.Dispose();
bg = null;
}
}
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_ERASEBKGND:
if (!DoubleBuffered)
{
m.Result = (IntPtr)1;
return;
}
break;
case WM_PAINT:
if (!DoubleBuffered)
{
PAINTSTRUCT PS = new PAINTSTRUCT();
IntPtr hdc = BeginPaint(Handle, ref PS);
try
{
if (bg == null)
bg = BufferedGraphicsManager.Current.Allocate
(hdc, ClientRectangle);
IntPtr bgHdc = bg.Graphics.GetHdc();
SendMessage(Handle, WM_PRINT, bgHdc, (IntPtr)PRF_CLIENT);
bg.Graphics.ReleaseHdc(bgHdc);
bg.Render(hdc);
}
finally
{
EndPaint(Handle, ref PS);
}
return;
}
break;
}
base.WndProc(ref m);
}
最终解决方案
现在我有一个可行的解决方案,但我想让它更优雅,并减少原生互操作。经过进一步研究,我决定使用内部控件逻辑来实现这一点。Control
类具有 UserPaint
样式,此样式意味着所有绘制都在用户代码中进行,并且不会调用底层控件的绘制。所以我设置了 UserPaint
样式,以及 OptimizedDoubleBuffer
和 AllPaintInWmPaint
(这些样式启用了内部控件双缓冲支持)。设置好这些样式后,我只需要重写控件的 OnPaint
方法,然后简单地使用 WM_PRINT
消息绘制控件。听起来不错,但不起作用,因为具有 UserPaint
样式的控件不仅会拦截默认绘制,还会拦截默认打印。所以,我创建了一个虚拟的 WM_PRINTCLIENT
消息,并直接将其分派给默认窗口过程,而不是发送 WM_PRINT
消息。
public DbTreeView()
{
// Enable default double buffering processing (DoubleBuffered returns true)
SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint, true);
// Disable default CommCtrl painting on non-Vista systems
if (Environment.OSVersion.Version.Major < 6)
SetStyle(ControlStyles.UserPaint, true);
}
protected override void OnPaint(PaintEventArgs e)
{
if (GetStyle(ControlStyles.UserPaint))
{
Message m = new Message();
m.HWnd = Handle;
m.Msg = WM_PRINTCLIENT;
m.WParam = e.Graphics.GetHdc();
m.LParam = (IntPtr)PRF_CLIENT;
DefWndProc(ref m);
e.Graphics.ReleaseHdc(m.WParam);
}
base.OnPaint(e);
}
ListView
同样的方法也适用于 ListView
,但有一个例外:从 Windows XP 开始,ListView
可用原生双缓冲。而且 .NET 2 ListView
控件中甚至有更多的原生双缓冲支持,我只需要启用双缓冲即可。
public DbListView()
{
// Enable internal ListView double-buffering
SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint, true);
}
对于旧系统,我使用了与 treeview
完全相同的解决方案。
已知问题
- 由于 CommCtrl
TreeView
控件在 Windows 2000 及更早版本中的一个 bug,如果使用默认窗口颜色,节点线和按钮会在黑色背景上绘制。为了避免此 bug,我们在控件创建后显式设置背景颜色。 - Windows 2000 及更早版本上的双缓冲
ListView
在 Details 模式下仍然存在一些闪烁。这是因为listview
的标题是另一个没有双缓冲的控件。这对我来说不是问题,所以我没有动标题。这个问题可以通过子类化默认的listview
标题,并实现“第三次尝试”部分所述的解决方案来修复。
历史
- 2009 年 6 月 15 日: 初始发布