Search code examples
c#.netwinformspinvoke

Effect of temporary variable on erasing the background of a GroupBox


The context:

Consider drawing a GroupBox with a gradient as a part of it's background.

Example:

Ideal <code>GroupBox</code>


Let's perform the following actions:

  • Create a class that inherits GroupBox.
    • Set it's FlatStyle property to FlatStyle.System.
    • override it's WndProc method.
    • Handle the WM_ERASEBKGND message, in which we draw the gradient.
    • Handle the WM_PRINTCLIENT message, where we call DefWndProc and return.
      (Will be needed later.)
  • Add a Label as it's child Control.
    (The Label's background must be transparent to be able to see the gradient behind it's Text.

    • Create a class that inherits Label.
    • override the WndProc method.
    • "Simulate transparency" by calling the DrawThemeParentBackground function to draw the GroupBox's background on the Label's Graphics.

The issue:

Depending on whether a temporary variable is used to hold the Graphics object, the end result varies, depicted with the code sample and image below:

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace MCVE
{
    class GroupBox : System.Windows.Forms.GroupBox
    {
        const int WM_ERASEBKGND = 0x14;
        const int WM_PRINTCLIENT = 0x318;

        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case WM_ERASEBKGND:
                    base.WndProc(ref m);
                    using (var g = Graphics.FromHdc(m.WParam))//CASE 1
                    //using (var e = new PaintEventArgs(Graphics.FromHdc(m.WParam), ClientRectangle))//CASE 2
                    {
                        var e = new PaintEventArgs(g, ClientRectangle);//CASE 1
                        var r = new Rectangle(2, 12, Width - 4, Height - 2);
                        using (var b = new LinearGradientBrush(r, BackColor, SystemColors.Window, LinearGradientMode.Vertical))
                        {
                            e.Graphics.FillRectangle(b, r);//Draw the gradient.
                        }
                    }
                    m.Result = new IntPtr(1);//Signal that no further drawing of the background is necessary by WM_PAINT.
                    return;
                case WM_PRINTCLIENT:
                    DefWndProc(ref m);//Bypass GroupBox's internal handling so that actual painting is handled by Windows.
                    return;
            }
            base.WndProc(ref m);//Default processing of the rest of the messages.
        }
    };

    class Label : System.Windows.Forms.Label
    {
        const int WM_ERASEBKGND = 0x14;
        const int WM_PAINT = 0xF;

        [DllImport("user32.dll")] static extern IntPtr BeginPaint(IntPtr hWnd, out PAINTSTRUCT lpPaint);
        [DllImport("user32.dll")] static extern IntPtr EndPaint(IntPtr hWnd, ref PAINTSTRUCT lpPaint);
        //Ask Windows to send a message to the parent to draw it's background in the current device context.
        [DllImport("uxtheme.dll")] extern static int DrawThemeParentBackground(IntPtr hWnd, IntPtr hdc, ref Rectangle pRect);

        [StructLayout(LayoutKind.Sequential)]
        struct PAINTSTRUCT
        {
            public IntPtr hdc;
            public bool fErase;
            public Rectangle rcPaint;
            public bool fRestore;
            public bool fIncUpdate;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public byte[] rgcReserved;
        };

        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case WM_ERASEBKGND:
                    var r = ClientRectangle;
                    DrawThemeParentBackground(Handle, m.WParam, ref r);
                    m.Result = new IntPtr(1);//Signal that no further drawing of the background is necessary by WM_PAINT.
                    return;
                case WM_PAINT:
                    PAINTSTRUCT ps;
                    var hdc = BeginPaint(Handle, out ps);
                    EndPaint(Handle, ref ps);//Don't paint any text so that the gradient remains visible.
                    m.Result = IntPtr.Zero;
                    return;
            }
            base.WndProc(ref m);//Default processing of the rest of the messages.
        }
    };

    static class Program
    {
        [STAThread] static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var form = new Form() { BackColor = SystemColors.Highlight };
            var groupbox = new GroupBox() { Anchor = (AnchorStyles)15, FlatStyle = FlatStyle.System, Location = new Point(10, 10), Text = "groupBox1" };

            form.Controls.Add(groupbox);
            groupbox.Controls.Add(new Label() { FlatStyle = FlatStyle.System, Location = new Point(50, 50) });

            Application.Run(form);
        }
    };
}

Running the above MCVE (CASE 1) produces the expected ouput as shown in the example image.

On commenting out the lines remarked CASE 1 and uncommenting the line marked CASE 2 gives the following undesired output:

Actual <code>GroupBox</code>

The question:

Why does the removal of the temporary variable produce such a vastly different output?


Solution

  • Posting this as an answer for the sake of completeness.

    • As pointed out by Ivan Stoev, the non-owning PaintEventArgs does not call Dispose on the Graphics object.
    • This has visible side effects as the DC is reused by Windows in the WM_PRINTCLIENT Message, that is sent next to the WndProc.

    Manually calling Dispose on the Graphics object confirms this.

    using (var g = Graphics.FromHdc(m.WParam))
    {
        using (var e = new PaintEventArgs(g, ClientRectangle))
        {
            var r = new Rectangle(2, 12, Width - 4, Height - 2);
            using (var b = new LinearGradientBrush(r, BackColor, SystemColors.Window, LinearGradientMode.Vertical))
            {
                e.Graphics.FillRectangle(b, r);//Draw the gradient.
            }
        }
    }