65.9K
CodeProject 正在变化。 阅读更多。
Home

双缓冲的树形和列表视图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (21投票s)

2009年6月15日

MIT

4分钟阅读

viewsIcon

129676

downloadIcon

3249

实现原生 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 日: 初始发布
© . All rights reserved.