Panel border size and border color – Customize nonclient area

In this post, I’ll show you how you can customize the panel border size and border color by customizing the nonclient area of the panel.

The following picture shows panels with different border color and border size:

Panel border size and border color

When you set BorderStyle of a standard panel control to FixedSingle, you see a black 1-pixel border around the panel. This black border is nonclient area for the panel:

  • It’s not on the client rectangle, which means none of the controls of the form can cover the border
  • You cannot paint on that area in OnPaint method or Paint event
  • You cannot change it’s width using common properties of the panel.

To modify the size and appearance of the nonclient area, you need to override the WndProc and handle some windows messages. Here we are especially interested in the following messages:

You also need to make sure when the control size changes, or when you modify the border width of the control, or when you mofidy the border color, the correct client and nonclient area will be calculated correctlty and both areas will be repainted.

Change the size of nonclien area

To change the size and appearance of the border, you first need to handle WM_NCCALCSIZE message get an instance of NCCALCSIZE_PARAMS structure from the lparam of the message. The rgrc member of the strcuture is an array of
RECT structure. The first rectangle containse the bounds of the rectanle; then if you modify it and return zero as the result of the message, the default window procedure will use this rect as the client rectange of the control and difference between original value and the new value will be used as nonclient area:

if (m.WParam != IntPtr.Zero)
{
    var nccsp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));
    nccsp.rgrc[0].top += borderWidth - 1;
    nccsp.rgrc[0].bottom -= borderWidth - 1;
    nccsp.rgrc[0].left += borderWidth - 1;
    nccsp.rgrc[0].right -= borderWidth - 1;
    Marshal.StructureToPtr(nccsp, m.LParam, true);
    InvalidateRect(this.Handle, nccsp.rgrc[0], true);
    m.Result = IntPtr.Zero;
}

Change appearance of the nonclient area

To change the appearance of nonclient area you need to handle WM_NCPAINT. Then you can get the device context handler using GetWindowDC and pass it to Graphics.FromHdc to get graphics object and paint on non-clien area:

var dc = GetWindowDC(Handle);
using (var g = Graphics.FromHdc(dc))
{
    using (var p = new Pen(BorderColor, borderWidth) { Alignment = PenAlignment.Inset })
    {
        if (VScroll && HScroll)
        {
            Rectangle bottomCornerRectangle = new Rectangle(
                Width - SystemInformation.VerticalScrollBarWidth - borderWidth,
                Height - SystemInformation.HorizontalScrollBarHeight - borderWidth,
                SystemInformation.VerticalScrollBarWidth,
                SystemInformation.HorizontalScrollBarHeight);
            if (RightToLeft == RightToLeft.Yes)
            {
                bottomCornerRectangle.X = Width - bottomCornerRectangle.Right;
            }
            g.FillRectangle(SystemBrushes.Control, bottomCornerRectangle);
        }
        var adjustment = borderWidth == 1 ? 1 : 0;
        g.DrawRectangle(p, 0, 0, Width - adjustment, Height - adjustment);
    }
}
ReleaseDC(Handle, dc);

Invalidate the size and appearance of nonclient area

If you want to have a BorderWidth property, it should change the size of the nonclient area without resizing the control. To do this, you can call
SetWindowPos by setting the SWP_FRAMECHANGED flag.

SetWindowPos(this.Handle, IntPtr.Zero, 0, 0, 0, 0,
    SWP_NOSIZE | SWP_NOMOVE | SWP_FRAMECHANGED | SWP_NOZOORDER);

If you want to have a BorderColor property, when you change it, it should repaint the nonclient area; however non of the Refresh, Invalidate, or Update method repaints nonclient area. To push repainting of the nonclient area, you need to RedrawWindow by setting the SWP_FRAMECHANGED flag.

RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero,
   RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW);

Download

You can clone or download the code:

You May Also Like

About the Author: Reza Aghaei

I’ve been a .NET developer since 2004. During these years, as a developer, technical lead and architect, I’ve helped organizations and development teams in design and development of different kind of applications including LOB applications, Web and Windows application frameworks and RAD tools. As a teacher and mentor, I’ve trained tens of developers in C#, ASP.NET MVC and Windows Forms. As an interviewer I’ve helped organizations to assess and hire tens of qualified developers. I really enjoy learning new things, problem solving, knowledge sharing and helping other developers. I'm usually active in .NET related tags in stackoverflow to answer community questions. I also share technical blog posts in my blog as well as sharing sample codes in GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *