临时变量对擦除 GroupBox 背景的影响

Effect of temporary variable on erasing the background of a GroupBox

上下文:

考虑绘制一个 GroupBox 并将渐变作为其背景的一部分。

示例:


让我们执行以下操作:

问题:

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);
        }
    };
}

运行 上面的 MCVE (CASE 1) 产生了示例图像中所示的预期输出。

注释掉标记为 CASE 1 的行并取消注释标记为 CASE 2 的行会给出以下不需要的输出:

问题:

为什么删除临时变量会产生如此大不相同的输出?

所有这些都可以归结为一个没有 PInvoking 的简单派生 class 吗? 此class源自GroupBox,使用CreateParams CreateParams.ExStyle proeprty, enables support for Transparent colors with Control.SetStyle() and ControlStyles.SupportsTransparentBackColor style (this way you can better control the effect of the LinearGradientBrush) and overrides the OnPaintBackground()方法将其设置为透明进行绘画。

这只是一个基本示例,但可以通过任何其他方式对其进行调整,并且仍然更加便携。
您可以在其上放置任何控件。

class GradientGroupBox : GroupBox
{
    private const int WS_EX_TRANSPARENT = 0x20;
    public GradientGroupBox() => this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);

    protected override void OnPaintBackground(PaintEventArgs e)
    {
        base.OnPaintBackground(e);
        Color gradFillTo = Color.FromArgb(200, SystemColors.Window);
        Color gradFillFrom = Color.FromArgb(128, this.Parent.BackColor);
        using (LinearGradientBrush gradientBrush = new LinearGradientBrush(this.ClientRectangle, gradFillFrom, gradFillTo, LinearGradientMode.Vertical))
        {
            e.Graphics.FillRectangle(gradientBrush, this.ClientRectangle);
        }
    }

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams parameters = base.CreateParams;
            parameters.ExStyle |= WS_EX_TRANSPARENT;
            return parameters;
        }
    }
}


为了完整起见,将此作为答案发布。

  • 正如 Ivan Stoev 所指出的,非所有者 PaintEventArgs 不会 调用 DisposeGraphics object.
  • 这有可见的副作用,因为 DC 在 WM_PRINTCLIENT 中被 Windows 重用 Message,即在WndProc.
  • 旁边发送

Graphics object 上手动调用 Dispose 确认了这一点。

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.
        }
    }
}