Click here to Skip to main content
15,867,141 members
Articles / Desktop Programming / Windows Forms

Double-buffered Tree and Listviews

Rate me:
Please Sign up or sign in to vote.
4.94/5 (22 votes)
15 Jun 2009MIT4 min read 125.1K   3.2K   102   29
Implementing native WinForms flicker-free TreeView and ListView descendants

Introduction

In this article, I will describe implementing native WinForms TreeView and ListView descendants that are flicker-free. The 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 to find workarounds for this behavior and eliminate flickering by using double buffering. When I first noticed this problem, I did some Googling and didn't find any solutions, so I have invented several solutions by myself.

First Attempt with TreeView

Let me first mention why this problem occurs. In general, control painting cycle consists of two phases: erasing and painting. First Windows sends a message to erase the background, then to paint. When the control receives erase message, if fill control surface with solid color, then control draws 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 treeview labels from painting cycle by using clipping region. 

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

C#
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 started moving my project to Windows Vista, I found that Microsoft implemented native double-buffering, and my task was only to enable such functionality. To do this, I would send TVM_SETEXTENDEDSTYLE message with TVS_EX_DOUBLEBUFFER style after control windows handle creation. This worked very well, but only under Windows Vista.

C#
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 started thinking about older systems, especially Windows XP, because it has a large user base. So I have returned to my first attempt and started investigation. First I tried to substitute owner draw hdc by overriding NM_CUSTOMDRAW message, but without luck (it seems than Windows does not detect handle substitution and ignores it).

Then I remembered the WM_PRINT message. This message is used to draw a 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?

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

C#
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 a 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 these styles, all I need is to override OnPaint control and simply paint control using WM_PRINT message. Sounds good, but does not work, because control with UserPaint style intercepts 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.

C#
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 exists in .NET 2 ListView control, all I need is to enable double-buffering.

C#
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 treeview.

Known Issues

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

History

  • 15th June, 2009: Initial post

License

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


Written By
Architect
Belarus Belarus
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.

Comments and Discussions

 
Generalthanks Pin
woyaowan123415-Oct-11 17:29
woyaowan123415-Oct-11 17:29 
GeneralRe: thanks Pin
pjsr6-Feb-12 10:48
pjsr6-Feb-12 10:48 
GeneralRe: thanks Pin
Roger5008-Feb-12 21:23
Roger5008-Feb-12 21:23 
GeneralRe: thanks Pin
pjsr9-Feb-12 6:25
pjsr9-Feb-12 6:25 
GeneralYou saved me a lot of work. Thanks! Pin
Roger50023-Jan-11 19:53
Roger50023-Jan-11 19:53 
GeneralSpeedup: the hDC isn't clipped Pin
tom ireland4-Nov-10 23:49
tom ireland4-Nov-10 23:49 
QuestionHow to change font of tree view? Pin
Yakov Maksheiev20-Apr-10 20:14
Yakov Maksheiev20-Apr-10 20:14 
AnswerRe: How to change font of tree view? Pin
firda-cze5-Mar-15 23:09
firda-cze5-Mar-15 23:09 
Generalthis.DoubleBuffered = true; Pin
yukchan14-Jan-10 15:53
yukchan14-Jan-10 15:53 
GeneralRe: this.DoubleBuffered = true; Pin
Eugene Sichkar17-Jan-10 2:04
Eugene Sichkar17-Jan-10 2:04 
GeneralFlicker on ListView Pin
black198115-Dec-09 7:07
black198115-Dec-09 7:07 
GeneralRe: Flicker on ListView Pin
Eugene Sichkar15-Dec-09 10:34
Eugene Sichkar15-Dec-09 10:34 
Generalcompact frameowrk Pin
JossGP3-Dec-09 22:51
JossGP3-Dec-09 22:51 
GeneralRe: compact frameowrk Pin
Eugene Sichkar15-Dec-09 10:29
Eugene Sichkar15-Dec-09 10:29 
GeneralМолодец. Pin
drweb8619-Oct-09 2:50
drweb8619-Oct-09 2:50 
GeneralText Font and Size... Pin
Member 221447122-Sep-09 10:23
Member 221447122-Sep-09 10:23 
GeneralRe: Text Font and Size... [modified] Pin
Ulric1-Jul-10 7:20
Ulric1-Jul-10 7:20 
GeneralSpeeding up (native) TreeView painting Pin
OrlandoCurioso6-Jul-09 5:22
OrlandoCurioso6-Jul-09 5:22 
GeneralRe: Speeding up (native) TreeView painting Pin
Eugene Sichkar22-Sep-09 11:46
Eugene Sichkar22-Sep-09 11:46 
GeneralRe: Speeding up (native) TreeView painting Pin
Rolf Kristensen23-Sep-09 5:32
Rolf Kristensen23-Sep-09 5:32 
GeneralRe: Speeding up (native) TreeView painting [modified] Pin
Mikael Wiberg19-Dec-09 23:19
Mikael Wiberg19-Dec-09 23:19 
AnswerSpeeding up... plus individual NodeFont Pin
OrlandoCurioso4-Jan-10 1:58
OrlandoCurioso4-Jan-10 1:58 
GeneralCongratulations Pin
OrlandoCurioso6-Jul-09 0:23
OrlandoCurioso6-Jul-09 0:23 
Generalscrolling problem Pin
airlobster24-Jun-09 5:36
airlobster24-Jun-09 5:36 
QuestionRe: scrolling problem Pin
Eugene Sichkar24-Jun-09 7:45
Eugene Sichkar24-Jun-09 7:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.