Double-buffered tree and list views

Introduction

In this article I will describe implementing native WinForms TreeView and ListView descedants that are flicker-free. Solution will be described in several iterations which are connected to actual development in time. All this is created for and used in my freeware file manager Nomad.NET

The Problem

If you ever use default WinForms TreeView or ListView controls you already notice massive flickering when control size changes (for example when you change form size with mouse). In this article I will try find workarounds for this behavior and eliminate flickering by using double buffering. When I first notice this problem, I was made some googling and didn't find any solutions, so I have invented several solutions by myself.

First Attempt with TreeView

Let me first say, why this problem occurs. In general control painting cycle consist from two phases: erasing and painting. First Windows send message to erase background, then to paint. When control receives erase message if fill control surface with solid color, then control draw content on the surface. When user changes form size, Windows invokes many painting cycles, and in every cycle erase and painting occurs, but because painting occurs with delay after erasing, user will see noticeable flickering.

So my first idea was to eat erasing messages and exclude tree view labels from painting cycle by using clipping region. 

To do this I override default WndProc method, catch WM_ERASEBKGND and WM_PAINT messages. This solution works well for me for about a year, but some parts still flicker (tree lines, buttons and icons), and sometimes (rarely) selected node painted with glitches. 

Collapse Copy Code
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);
}

Second Attempt

When I start moving my project to Windows Vista, I have found that Microsoft was implemented native double-buffering, and my task is only to enable such functionality. To do this I will send TVM_SETEXTENDEDSTYLE message with TVS_EX_DOUBLEBUFFER style after control windows handle creation. This works very well but only under Windows Vista.

Collapse Copy Code
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();
}

Third Attempt

After implementing native double-buffering under Vista, I start thinking about older systems, especially Windows XP, because it has large user base. So I have returned to my first attempt and start investigation. First I was try to substitute owner draw hdc, by overriding NM_CUSTOMDRAW message, but without luck (it seems than Windows do not detect handle substitution and ignores it).

Then I remembered about WM_PRINT message. This message used to draw control on the user supplied surface and usually used for printing. But why not use this message to draw control on bitmap, and then draw this bitmap in the paint cycle?

First version uses background bitmap for offscreen painting, and it works! After proving concept I started optimization process, and bitmap was replaced with .NET BufferedGraphics class. This class is specially created for implementing double-buffering in controls.

Collapse Copy Code
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);
}

Final Solution

Now I have working solution, but I want to make it more elegant and with less native interoperability. After additional investigation I have decided to use internal Control logic to do this. Control class has UserPaint style and this style means that all painting occurs in user code, and underlying control painting is not invoked at all. So I have set UserPaint style, as well as OptimizedDoubleBuffer and AllPaintInWmPaint (these styles enable internal control double buffering support). After setting this styles all I need is to override OnPaint control and simply paint control using WM_PRINT message. Sounds good, but not works, because control with UserPaint style intercept not only default painting but also default printing. So instead of sending WM_PRINT message, I have created dummy WM_PRINTCLIENT message and dispatch it directly to default window procedure.

Collapse Copy Code
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

The same approach is valid for ListView, with one exception, native double buffering is available for ListView starting from Windows XP. And even more support for native double buffering are exists in .net 2 ListView control, all I need is to enable double-buffering.

Collapse Copy Code
public DbListView()
{
    // Enable internal ListView double-buffering
    SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
}

For older systems I have used exactly the same solution as for tree view.

Known Issues

  • Due to bug in CommCtrl TreeView control on Windows 2000 and earlier, node lines and buttons drawn on black surface if default window color is used. To avoid this bug we simple explicitly set background color after control creation.
  • Double-buffered ListView on Windows 2000 and earlier still have some flickering in Details mode. This occurs because list view header is another control without double buffering. It is not an issue for me, so I leave header untouched. This issue can be fixed by subclass default list view header, and implementing kind of solution described in "Third attempt" section.

License

This article, along with any associated source code and files, is licensed under The MIT License

About the Author

Eugene Sichkar


Member
My English is not very good and I know this. So, if you find any translation bugs, misspelled words or sentences on these pages, please, drop a line to my email.
Occupation: Architect
Location: Belarus Belarus
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值