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.
using System.Drawing;
using System.Windows.Forms;
namespace System.Windows.Forms
{
public class PanelEx : Panel
{
public PanelEx()
{
SetStyle(ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer
| ControlStyles.ResizeRedraw
| ControlStyles.UserPaint
,true);
}
protected override void OnPaintBackground(PaintEventArgs e) {
e.Graphics.Clear(BackColor);
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:
protected override void OnPaintBackground(PaintEventArgs e) {
e.Graphics.Clear(BackColor);
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).
using System.Drawing;
using System.Windows.Forms;
namespace System.Windows.Forms
{
public class PanelEx : Panel
{
public PanelEx()
{
SetStyle(ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer
| ControlStyles.ResizeRedraw
| ControlStyles.UserPaint
, true);
}
protected override void OnPaintBackground(PaintEventArgs e)
{
e.Graphics.Clear(BackColor);
ControlPaint.DrawBorder(e.Graphics, ClientRectangle, BackColor, ButtonBorderStyle.Inset);
}
public override Rectangle DisplayRectangle
{
get
{
Rectangle rect = base.DisplayRectangle;
rect.Inflate(-1, -1);
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.