Improving performance in Windows Forms by temporarily suspending redrawing a window

Sometimes the default way a window gets redrawn wreaks havoc with our code that’s updating the window, because the window gets redrawn at some point(s) when we have made some but not all of our changes. This can cause anything from windows that look like badly created stop-motion animations while they are being resized, to container windows that display child windows being added, then flicker and repaint as the child windows are reordered, to windows that look normal but are unresponsive to user input.

At such times, it would be good to have a way of suspending the painting of the respective windows, but all we get built-in for this is the SuspendLayoutResumeLayout pattern, which does not do quite what we need here.

A mistake many developers make here is calling LockWindowUpdate. Raymond Chen wrote a great series explaining why you shouldn’t do so, amongst other things, a few years ago.

If you have read the linked post, you know the solution already: WM_SETREDRAW

I wrote a helper class in c# to do the grunt work for me. It is available in my RomyView application.

In addition to wrapping the SendMessage call and reference counting it so that it can handle nested calls, the code that redraws the window uses the native RedrawWindow API function. This is because the function takes an options parameter that allows us to optimize the redrawing code, passing different flags for container windows to child windows.

The helper class has a SuspendDrawing and ResumeDrawing method, where ResumeDrawing has an overload that accepts a bool parameter to indicate whether the window is a container or child; as well as a ResumeDrawing overload that exposes the options as a parameter so that you can specify your own combination of flags for a specific window if needed.

Thus I use the code in a couple of similar, but subtly different places. For example:

  • When toggling the image viewer window between full screen and windowed mode – which not only resizes the window after changing the window state between normal and maximized, but also changes the border style as well as a few child-window properties. Without temporarily suspending the redrawing, a few horrendously ugly repaints were rendered, with the window in an intermediate state that I never even imagined, let alone expected to be visible.

  • I also use it to insert thumbnails into a FlowLayoutPanel. Of course ControlCollection doesn’t have an Insert method, but my custom collection does. The Insert implementation adds a thumbnail but prevents it from being drawn, then changes the thumbnail’s index and resumes it’s redrawing.

If you don’t want the whole application just for this code, here is the relevant class…

using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using System.Windows.Forms;

namespace Romy.Controls
{
    /// <summary>Allows suspending and resuming redrawing a Windows Forms window via the <b>WM_SETREDRAW</b> 
    /// Windows message.</summary>
    /// <remarks>Usage: The window for which drawing will be suspended and resumed needs to instantiate this type, 
    /// passing a reference to itself to the constructor, then call either of the public methods. For each call to 
    /// <b>SuspendDrawing</b>, a corresponding <b>ResumeDrawing</b> call must be made. Calls may be nested, but
    /// should not be made from any other than the GUI thread. (This code tries to work around such an error, but 
    /// is not guaranteed to succeed.)</remarks>
    public class DrawingHelper
    {
        #region Fields

        private int suspendCounter;

        private const int WM_SETREDRAW = 11;

        private IWin32Window owner;

        private SynchronizationContext synchronizationContext = SynchronizationContext.Current;

        #endregion Fields

        #region Constructors

        public DrawingHelper(IWin32Window owner)
        {
            this.owner = owner;
        }

        #endregion Constructors

        #region Methods

        /// <summary>This overload allows you to specify whether the optimal flags for a container 
        /// or child control should be used. To specify custom flags, use the overload that accepts 
        /// a <see cref="Romy.Controls.RedrawWindowFlags"/> parameter.</summary>
        /// <param name="isContainer">When <b>true</b>, the optimal flags for redrawing a container 
        /// control are used; otherwise the optimal flags for a child control are used.</param>
        public void ResumeDrawing(bool isContainer = false)
        {
            ResumeDrawing(isContainer ? RedrawWindowFlags.Erase | RedrawWindowFlags.Frame | RedrawWindowFlags.Invalidate | RedrawWindowFlags.AllChildren :
                RedrawWindowFlags.NoErase | RedrawWindowFlags.Invalidate | RedrawWindowFlags.InternalPaint);
        }

        [CLSCompliant(false)]
        public void ResumeDrawing(RedrawWindowFlags flags)
        {
            Interlocked.Decrement(ref suspendCounter);

            if (suspendCounter == 0)
            {
                Action resume = new Action(() =>
                {
                    NativeMethods.SendMessage(owner.Handle, WM_SETREDRAW, new IntPtr(1), IntPtr.Zero);
                    NativeMethods.RedrawWindow(owner.Handle, IntPtr.Zero, IntPtr.Zero, flags);
                });
                try { resume(); }
                catch (InvalidOperationException)
                {
                    synchronizationContext.Post(s => ((Action)s)(), resume);
                }
            }
        }

        public void SuspendDrawing()
        {
            try
            {
                if (suspendCounter == 0)
                {
                    Action suspend = new Action(() => NativeMethods.SendMessage(owner.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero));
                    try { suspend(); }

                    catch (InvalidOperationException)
                    {
                        synchronizationContext.Post(s => ((Action)s)(), suspend);
                    }
                }
            }
            finally { Interlocked.Increment(ref suspendCounter); }
        }

        #endregion Methods

        #region NativeMethods

        [SuppressUnmanagedCodeSecurity]
        internal static class NativeMethods
        {
            [DllImport("user32.dll")]
            public static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, RedrawWindowFlags flags);

            [DllImport("user32.dll")]
            public static extern IntPtr SendMessage(IntPtr hWnd, Int32 wMsg, IntPtr wParam, IntPtr lParam);
        }

        #endregion NativeMethods
    }

    #region RedrawWindowFlags

    [Flags(), CLSCompliant(false)]
    public enum RedrawWindowFlags : uint
    {
        ///<summary>Invalidates lprcUpdate or hrgnUpdate (only one may be non-NULL). 
        ///If both are NULL, the entire window is invalidated.</summary>
        Invalidate = 0x1,

        ///<summary>Causes a WM_PAINT message to be posted to the window regardless of 
        ///whether any portion of the window is invalid.</summary>
        InternalPaint = 0x2,

        ///<summary>Causes the window to receive a WM_ERASEBKGND message when the window 
        ///is repainted. The <b>Invalidate</b> flag must also be specified; otherwise, 
        ///<b>Erase</b> has no effect.</summary>
        Erase = 0x4,

        ///<summary>Validates lprcUpdate or hrgnUpdate (only one may be non-NULL). If both 
        ///are NULL, the entire window is validated. This flag does not affect internal 
        ///WM_PAINT messages.</summary>
        Validate = 0x8,

        ///<summary>Suppresses any pending internal WM_PAINT messages. This flag does not 
        ///affect WM_PAINT messages resulting from a non-NULL update area.</summary>
        NoInternalPaint = 0x10,

        ///<summary>Suppresses any pending WM_ERASEBKGND messages.</summary>
        NoErase = 0x20,

        ///<summary>Excludes child windows, if any, from the repainting operation.</summary>
        NoChildren = 0x40,

        ///<summary>Includes child windows, if any, in the repainting operation.</summary>
        AllChildren = 0x80,

        ///<summary>Causes the affected windows (as specified by the <b>AllChildren</b> and <b>NoChildren</b> flags) to 
        ///receive WM_NCPAINT, WM_ERASEBKGND, and WM_PAINT messages, if necessary, before the function returns.</summary>
        UpdateNow = 0x100,

        ///<summary>Causes the affected windows (as specified by the <b>AllChildren</b> and <b>NoChildren</b> flags) 
        ///to receive WM_NCPAINT and WM_ERASEBKGND messages, if necessary, before the function returns. 
        ///WM_PAINT messages are received at the ordinary time.</summary>
        EraseNow = 0x200,

        ///<summary>Causes any part of the nonclient area of the window that intersects the update region 
        ///to receive a WM_NCPAINT message. The <b>Invalidate</b> flag must also be specified; otherwise, 
        ///<b>Frame</b> has no effect. The WM_NCPAINT message is typically not sent during the execution of 
        ///RedrawWindow unless either <b>UpdateNow</b> or <b>EraseNow</b> is specified.</summary>
        Frame = 0x400,

        ///<summary>Suppresses any pending WM_NCPAINT messages. This flag must be used with <b>Validate</b> and 
        ///is typically used with <b>NoChildren</b>. <b>NoFrame</b> should be used with care, as it could cause parts 
        ///of a window to be painted improperly.</summary>
        NoFrame = 0x800
    }

    #endregion RedrawWindowFlags
}
Advertisements

About Jerome

I am a senior C# developer in Johannesburg, South Africa. I am also a recovering addict, who spent nearly eight years using methamphetamine. I write on my recovery blog about my lessons learned and sometimes give advice to others who have made similar mistakes, often from my viewpoint as an atheist, and I also write some C# programming articles on my programming blog.
This entry was posted in Programming and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s