Panel titlebar – Customize nonclient area

In this example I’ve added a titlebar to the panel by extending the nonclient area of the panel and painting on the nonclient area. It basically means the titlebart is not part of the client rectangle and it cannot be covered by any control.

The panel has derived from a standard panel and in addition to the standard properties, support the following properties:

  • Text: The title of the panel.
  • TitlebarForeColor: The foreground colore of the titlebar
  • TitlebarBackColor: The background colore of the titlebar. This color will be used for border as well.
  • TitlebarFont: The fornt of the title
  • TitlebarTextPadding: The padding around the title
  • ShowTitlebar: Determines visibility of the titlebar

Panel titlebar - nonclient area.

You can find the download link at bottom of the post.

Set title of the panel

The standard Panel control has a Text property, however, since there hasn’t been any usage for that property it’s been markes as non-browsable. We override this property, and make it visible.

[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
[Bindable(true)]
public override string Text
{
    get
    {
        return base.Text;
    }
    set
    {
        base.Text = value;
        Redraw();
    }
}

When modifying something in nonclient area, we need to redraw the nonclient area, which will be done by calling RedrawWindow and setting the SWP_FRAMECHANGED flag.

Set the backcolor and forecolor of the title
TitlebarForeColor and TitlebarBackColor properties properties determine the background and foreground color for the titlebar. I’ve set black as the default value for the back color and white for the fore color. I also have used titlebar back color as border color of the control.

private Color titlebarBackColor = Color.Black;
[DefaultValue(typeof(Color), "Black")]
public Color TitlebarBackColor
{
    get { return titlebarBackColor; }
    set
    {
        if (titlebarBackColor != value)
        {
            titlebarBackColor = value;
            Redraw();
        }
    }
}

private Color titlebarForeColor = Color.White;
[DefaultValue(typeof(Color), "White")]
public Color TitlebarForeColor
{
    get { return titlebarForeColor; }
    set
    {
        if (titlebarForeColor != value)
        {
            titlebarForeColor = value;
            Redraw();
        }
    }
}

Show of hide the titlebar
The ShowTitlebar property determines the visibility of the titlebar. The default values for this property is true.

private bool showTitlebar = true;
[DefaultValue(true)]
public bool ShowTitlebar
{
    get { return showTitlebar; }
    set
    {
        if (showTitlebar != value)
        {
            showTitlebar = value;
            RecalculateClientSize();
            Redraw();
        }
    }
}

Since the titlebar is part of the nonclient area, if you toggle the visibility of it, then we need to recalculate the client size and nonclient size and redras the control. To do this, you can call
SetWindowPos by setting the SWP_FRAMECHANGED flag.

Font of the titlebar

There’s a TitlebarFont property which determines the font which we use to paint the title text:

private Font titlebarFont = DefaultFont;
public virtual Font TitlebarFont
{
    get
    {
        return titlebarFont;
    }
    set
    {
        if (titlebarFont != value)
        {
            titlebarFont = value;
            RecalculateClientSize();
            Redraw();
        }
    }
}
private bool ShouldSerializeTitlebarFont()
{
    return TitlebarFont != DefaultFont;
}
private void ResetTitlebarFont()
{
    TitlebarFont = DefaultFont;
}

The default value of the font property has been set to Control.DefaultFont.

Usually you can control the default value of the control by using p[DefaultValue] attribute; this will be useful for two purpose:

  • When the value of the property equals to the default value, the designer will not serialize it.
  • When you right click on the property in the property grid and choose Reset, the value of the property will be reset to the default value.

The [DefaultValue] attribute can work in cases that the default value is a static value, but in case thet you want to rely to another property, like in this case, using Control.DefaultFont as the default value, then assuming you have a property calles XXXX, then you need to have the following methods to support default value:

  • bool ShouldSerializeXXXX(): Tells to the designer when to serialize the property
  • void ResetXXXX(): Will reset the property

Please keep in mind, the default value attribute or the above method doesn’t set the default value, but control the behavior of the designer with default values. You need to assign the default value to the property yourself.

Height of the titlebar

For this example, I decided to calculate the height of the titlebar based on the font of the titlebar. Also I added a padding prroperty to allow the developer to add a bit more space around the text. So in general, a larger font will result in a larger titlebar. Also if you need a bit more padding around the text, you can set TitlebarTextPadding property to the desired value.

private Padding titlebarTextPadding = new Padding(5, 10, 5, 10);
[DefaultValue(typeof(Padding), "5,10,5,10")]
public virtual Padding TitlebarTextPadding
{
    get
    {
        return titlebarTextPadding;
    }
    set
    {
        if (titlebarTextPadding != value)
        {
            titlebarTextPadding = value;
            RecalculateClientSize();
            Redraw();
        }
    }
}

public virtual int GetTitlebarHeight()
{
    return (int)TitlebarFont.GetHeight() +
        titlebarTextPadding.Top +
        titlebarTextPadding.Bottom;
}

public virtual Rectangle GetTitlebarRectangle()
{
    return new Rectangle(0, 0, Width, GetTitlebarHeight());
}

Handling WM_NCPAINT and WM_NCCALCSIZE

Since we decided to have the titlebar as nonclient area, the like what I explained in the previouis post, we need to handle the following messages:

When handling the WM_NCCALCSIZE, we set the size and location of the nonclient area of the control, then the whole remaining area will be considered as nonclient area, for example assuming you have R1 as the whole area of the rectangle and you define R2 as the client area, then the area which is determined by R1-R2 or R1.Exclude(R2) will be the nonclient area.

To draw on the nonclient area, we need to hanlde WM_NCPAINT and get the graphics object for the surface of the control and draw on it.

private void WmNCCalcSize(ref Message m)
{
    var h = ShowTitlebar ? GetTitlebarHeight() : 0;
    var b = BorderStyle == BorderStyle.FixedSingle ? 1 :
        BorderStyle == BorderStyle.Fixed3D ? 2 : 0;

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

private void WmNCPaint(ref Message m)
{
    if (!ShowTitlebar)
        return;

    var dc = GetWindowDC(Handle);
    using (var g = Graphics.FromHdc(dc))
    {
        using (var b = new SolidBrush(TitlebarBackColor))
            g.FillRectangle(b, GetTitlebarRectangle());
        if (BorderStyle != BorderStyle.None)
            using (var p = new Pen(TitlebarBackColor))
                g.DrawRectangle(p, 0, 0,
                    Width - 1, Height - 1);

        var tf = TextFormatFlags.NoPadding | TextFormatFlags.VerticalCenter;
        if (RightToLeft == RightToLeft.Yes)
            tf |= TextFormatFlags.Right | TextFormatFlags.RightToLeft;
        var t = GetTitlebarRectangle();
        var r = new Rectangle(
            t.Left + TitlebarTextPadding.Left,
            t.Top,
            t.Width - TitlebarTextPadding.Left - TitlebarTextPadding.Right,
            t.Height);
        TextRenderer.DrawText(g, Text, TitlebarFont, r, TitlebarForeColor, tf);
    }
    ReleaseDC(Handle, dc);
    m.Result = IntPtr.Zero;
}

Fix the AutoScroll and AutoSize behavior
We need to fix the auto-scroll size and right-to-left behavior of the control. Since we have added a titlebar, then we need to fix the calculations of the auto-scorll size for the control to include the height of the titlebar in their calculation, when the titlebar is visible.

The method which is responsible for this calculation is GetPreferredSize:

public override Size GetPreferredSize(Size proposedSize)
{
    var size = base.GetPreferredSize(proposedSize);
    if (ShowTitlebar)
        size.Height += GetTitlebarHeight();
    return size;
}

Redraw and recalculate client size of the control

In some cases we need to redraw the nonclient area, for example when the Size, ot TitlebarBackColor, or TitlebarForeColor, or Text changes. Also in some cases we need to recalculatre the client and nonclient size of the control, for exampkle when the TitlebarFont of the titlebar changes or when RIghtToLeft changes:

protected override void OnRightToLeftChanged(EventArgs e)
{
    base.OnRightToLeftChanged(e);
    RecalculateClientSize();
    Redraw();
}
protected override void OnSizeChanged(EventArgs e)
{
    base.OnSizeChanged(e);
    Redraw();
}

private void Redraw()
{
    RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero,
        RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW);
}

private void RecalculateClientSize()
{
    SetWindowPos(this.Handle, IntPtr.Zero, 0, 0, 0, 0,
        SWP_NOSIZE | SWP_NOMOVE | SWP_FRAMECHANGED | SWP_NOZOORDER);
}

Downlaod

You can clone the repository or download the code of this example:

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.

3 Comments

  1. Hello Reza!!!

    I just downloaded your project to study it and when I open it with VS I get this error:

    Severity Code Description Project File Line Suppression State
    Error MSB4018 The “ResolvePackageAssets” task failed unexpectedly.
    NuGet.Packaging.Core.PackagingException: Unable to find fallback package folder ‘C:\Program Files\DevExpress 22.1\Components\Offline Packages’.
    at NuGet.Packaging.FallbackPackagePathResolver..ctor(String userPackageFolder, IEnumerable`1 fallbackPackageFolders)
    at Microsoft.NET.Build.Tasks.NuGetPackageResolver.CreateResolver(IEnumerable`1 packageFolders)
    at Microsoft.NET.Build.Tasks.ResolvePackageAssets.CacheWriter..ctor(ResolvePackageAssets task)
    at Microsoft.NET.Build.Tasks.ResolvePackageAssets.CacheReader.CreateReaderFromDisk(ResolvePackageAssets task, Byte[] settingsHash)
    at Microsoft.NET.Build.Tasks.ResolvePackageAssets.CacheReader..ctor(ResolvePackageAssets task)
    at Microsoft.NET.Build.Tasks.ResolvePackageAssets.ReadItemGroups()
    at Microsoft.NET.Build.Tasks.ResolvePackageAssets.ExecuteCore()
    at Microsoft.NET.Build.Tasks.TaskBase.Execute()
    at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
    at Microsoft.Build.BackEnd.TaskBuilder.d__26.MoveNext() PanelTitlebarExample C:\Program Files\dotnet\sdk\7.0.101\Sdks\Microsoft.NET.Sdk\targets\Microsoft.PackageDependencyResolution.targets 267

  2. Is this also possible for the main Form titlebar (the one which holds the form title and three buttons to minimize, maximize, close)? Let’s say I want to set the color of it to red.

  3. Hi, Reza! Thank you for the great example. Can you please explain how to handle mouse event in thin non-client area. I tried WM_NCMOUSEMOVE and other mouse messages but nothing works. WM_NCHITTEST gives 0 in WParam

Leave a Reply

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