Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

The Zen of WinForms Panel Sub-classing

5.00/5 (8 votes)
28 Jun 2016CPOL2 min read 14.6K  
...without interop!

Background

I’ve seen many heroic attempts of sub-classing WinForms Panel control; ranging from simply deriving a new class from Panel and adding the OnPaint event; via trying to hook into WM_NCCALCSIZE; to creating controls hosted inside controls to control the client area. You see, the problem with deriving from Panel is the layouting.

The Problem

Let us first demonstrate the problem.

C#
using System.Drawing;
using System.Windows.Forms;

namespace System.Windows.Forms // This is evil. You shouldn't do it.
{
    public class PanelEx : Panel
    {
        public PanelEx()
        {
            // Like...completely own this control.
            SetStyle(ControlStyles.AllPaintingInWmPaint
                | ControlStyles.OptimizedDoubleBuffer
                | ControlStyles.ResizeRedraw
                | ControlStyles.UserPaint
                ,true);
        }

        protected override void OnPaintBackground(PaintEventArgs e) {
            // Clear background.
            e.Graphics.Clear(BackColor);
            // Draw 3D border.
            ControlPaint.DrawBorder(e.Graphics, ClientRectangle, BackColor, ButtonBorderStyle.Inset);
        }
    }
}

This is a very simple Panel derived control with a sunken border. It is beautiful, it is minimal, and it will work until you add a child control to it and dock it on top. Then the child control will position itself to 0,0 and hide your border.

So what can we do about it? Well … it depends on what behaviour we want. If we want docked controls to make bordered area of panel smaller, then we simply use e.ClipRectangle instead of ClientRectangle. Like this:

C#
protected override void OnPaintBackground(PaintEventArgs e) {
    // Clear background.
    e.Graphics.Clear(BackColor);
    // Draw 3D border.
    ControlPaint.DrawBorder(e.Graphics, e.ClipRectangle, BackColor, ButtonBorderStyle.Inset);
}

Now, as you add new docked controls, the clipping rectangle for the actual panel “grows” smaller.

But what if we want docked controls to be inside the bordered panel, and not reduce it? Examples of such controls would be collapsible panels, data input prompts, frames, ruler grids, etc.

In this case, we need to somehow convince all child controls that their client rectangle is smaller. If you fought with the old breed, then you’re probably already thinking about processing the WM_NCCALCSIZE message and replacing the client rectangle with the one of your desire. And yes, that’s exactly what we are going to do[1]. But fortunately, it has become easier nowadays.

The Magic

The Panel control now has a property called DisplayRectangle. It holds the actual area available to clients. This handy property just happens to be virtual, i.e. overridable. Here’s a code fragment showing you how to provide your own version of this property which reduces client rectangle by 1 point (i.e., by our 3D border width).

C#
using System.Drawing;
using System.Windows.Forms;

namespace System.Windows.Forms // This is evil. You shouldn't do it.
{
    public class PanelEx : Panel
    {
        public PanelEx()
        {
            // Like...completely own this control.
            SetStyle(ControlStyles.AllPaintingInWmPaint
                | ControlStyles.OptimizedDoubleBuffer
                | ControlStyles.ResizeRedraw
                | ControlStyles.UserPaint
                , true);
        }

        protected override void OnPaintBackground(PaintEventArgs e)
        {
            // Clear background.
            e.Graphics.Clear(BackColor);
            // Draw 3D border. Try using e.ClipRectangle and ClientRectangle.
            ControlPaint.DrawBorder(e.Graphics, ClientRectangle, BackColor, ButtonBorderStyle.Inset);
        }

        public override Rectangle DisplayRectangle
        {
            get
            {
                Rectangle rect = base.DisplayRectangle; // Don't worry. It's a value type.
                rect.Inflate(-1, -1); // Exclude 3D border.
                return rect;
            }
        }
    }
}

[1] Technically, the Display rectangle is almost like the client rectangle. But not quite. It can be larger: for example, a scrolling control’s display rectangle contains scrollbar, but its client rectangle does not.

Points of Interest

Voilà! You now have a basic framework for creating all sorts of collapsible panels, data input prompts, frames, ruler grids, etc.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)