Click here to Skip to main content
15,879,474 members
Articles / Programming Languages / C#
Tip/Trick

The Zen of WinForms Panel Sub-classing

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
28 Jun 2016CPOL2 min read 14K   8   5
...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)


Written By
Founder Wischner Ltd
United Kingdom United Kingdom
Writing code since 1982 and still enjoying it.

Comments and Discussions

 
Questionscreenshots? Pin
kiquenet.com20-Dec-19 7:42
professionalkiquenet.com20-Dec-19 7:42 
GeneralMy vote of 5 Pin
Franc Morales28-Jun-16 22:21
Franc Morales28-Jun-16 22:21 
GeneralRe: My vote of 5 Pin
Tomaž Štih28-Jun-16 22:46
Tomaž Štih28-Jun-16 22:46 
GeneralRe: My vote of 5 Pin
Franc Morales28-Jun-16 23:33
Franc Morales28-Jun-16 23:33 
BugAlthough you can... Pin
Philippe Mori28-Jun-16 3:12
Philippe Mori28-Jun-16 3:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.