Painting the borders of a custom control using WM_NCPAINT

A demonstration program showing borders painted via WM_NCPAINT
A demonstration program showing borders painted via WM_NCPAINT

Over the years I've created a number of controls that require borders. Sometimes, I'll draw the borders manually as part of the normal user paint sequence. Other times I'll apply the WS_EX_CLIENTEDGE or WS_BORDER styles and let Windows handle it for me.

The advantage of the latter approach is that that means there is nothing I need to do; the borders will automatically paint, and as they are excluded from the normal client region of the control, I don't need to account for them when performing my own painting or positioning of child controls such as scroll bars.

The disadvantage is that these borders will be painted in the classic Windows 95 style without any theming.

While working on a control recently, I went with applying window styles for ease, but then decided I wanted to draw themed borders. This time I decided to try something new, and have Windows still manage the borders, but I would override its default painting with my own, using the WM_NCPAINT message.

Sidequest: Applying window styles

If your custom controls use UserControl as a base, this already has a BorderStyle property which creates a non-client frame. Most of the controls I create don't need the extra functionality of UserControl and so I mostly inherit from Control. By default this does not create a frame; the code below adds a BorderStyle property and sets the appropriate style when the window handle is created.

csharp
const int WS_BORDER = 0x00800000;

const int WS_EX_CLIENTEDGE = 0x00000200;

private BorderStyle _borderStyle;

[Category("Appearance")]
[DefaultValue(typeof(BorderStyle), "Fixed3D")]
public BorderStyle BorderStyle
{
  get => _borderStyle;
  set
  {
    if (_borderStyle != value)
    {
      _borderStyle = value;

      this.UpdateStyles();
    }
  }
}

protected override CreateParams CreateParams
{
  get
  {
    CreateParams createParams;

    createParams = base.CreateParams;
    createParams.ExStyle &= ~WS_EX_CLIENTEDGE;
    createParams.Style &= ~WS_BORDER;

    switch (_borderStyle)
    {
      case BorderStyle.Fixed3D:
        createParams.ExStyle |= WS_EX_CLIENTEDGE;
        break;

      case BorderStyle.FixedSingle:
        createParams.Style |= WS_BORDER;
        break;
    }

    return createParams;
  }
}

Here we have a fairly basic property definition, the only aspect you might not normally see is the call to UpdateStyles - this will cause the window styles to be reapplied via our overridden CreateParams method.

The CreateParams property is also something you might not see or need to use very often, and is used to set the underlying Win32 attributes of the window (remember that as far as Windows is concerned, your forms, controls, etc are all "windows"). I'm using it here to set the border styles, but you could also use it to specify the name of an existing class such as EDIT, although that doesn't come up as often - a topic for another day, perhaps.

In our override we first remove any existing border styles. There shouldn't be any set, but better to be sure. We then apply either a basic or an extended style depending on our property value. And with that done, Windows will create an appropriate frame and paint it for us, allowing me to move on with the rest of this article.

Introducing WM_NCPAINT

The WM_NCPAINT message is sent to a window when its frame must be painted. We can intercept this message via the WndProc method of our control and then perform the desired painting.

csharp
const int WM_NCPAINT = 0x0085;

protected override void WndProc(ref Message m)
{
  if (m.Msg == WM_NCPAINT)
  {
    this.WmNcPaint(ref m);
  }
  else
  {
    base.WndProc(ref m);
  }
}

private void WmNcPaint(ref Message m)
{    
  base.WndProc(ref m); // Just going back to Windows, for now
}

Getting the device context

Regardless of if we're going to using managed or unmanaged painting we need to start by getting the device context (DC). According to the documentation for WM_NCPAINT, I should have been able to use GetDCEx to do this, but in practice I found it always returned a null handle 1. Normally, I might use GetDC but this returns a DC for the client, and we need it for the non-client area. For this technique, I will instead call GetWindowDC - the DC returned by this API will allow painting in both the client and non-client areas. Once we have finished with a DC, it needs to be released via ReleaseDC.

csharp
[DllImport("user32.dll")]
static extern IntPtr GetWindowDC(IntPtr hWnd);

[DllImport("user32.dll")]
static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDc);

private void WmNcPaint(ref Message m)
{    
  IntPtr hdc;

  hdc = GetWindowDC(this.Handle);

  // TODO: Paint something

  ReleaseDC(this.Handle, hdc);
}

Painting in a mostly-managed way

Once we've got our DC, we can begin to paint. The easiest way is use use the managed API, e.g. the Graphics object. We can create an instance of a Graphics instance from a Win32 DC via the static FromHdc method.

csharp
      using (Graphics g = Graphics.FromHdc(hdc))
      {
        // TODO: Paint
      }

If we immediately start to paint here however, we'll run into two issues - firstly, the DC is for the entire window, so we could accidentally paint over the client area. This shouldn't matter too much as client painting will follow but if that itself is only partial, artefacts may be left behind (plus in my testing there was obvious flicker). The second issue is that properties such as Control.Size may not have been update at the point this message is received and so could return inaccurate values.

To resolve these issues we need to do a little more work to both determine the correct client region, and also to exclude this region from painting.

First, we use GetClientRect to get a RECT describing the client. Next, we will get the window rectangle via GetWindowRect. Note that the former always has a location of zero, whilst the latter includes the position of the window. Also note that unlike the .NET Rectangle structure, the Win32 rect is comprised of left, top, right (left + width) and bottom (top + height) values, not the more convenient width and height you may be used to from working solely with .NET.

With these values in hand, I calculate the position of the client area with the assumption that the horizontal and vertical margins are equidistant. Save storing the actual values retrieved or calculated via WM_NCCALCSIZE (which I will briefly cover later), I don't actually know how you'd get them another way2.

The new painting implementation is a little longer, but more robust.

csharp
[DllImport("user32.dll", SetLastError = true)]
static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);

[DllImport("user32.dll", SetLastError = true)]
static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

[StructLayout(LayoutKind.Sequential)]
struct RECT
{
  public int left;
  public int top;
  public int right;
  public int bottom;
}

private void WmNcPaint(ref Message m)
{    
  int w;
  int h;
  Rectangle clip;
  IntPtr hdc;

  GetClientRect(this.Handle, out RECT clientRect);
  GetWindowRect(this.Handle, out RECT windowRect);

  w = windowRect.right - windowRect.left;
  h = windowRect.bottom - windowRect.top;

  clip = new Rectangle((w - clientRect.right) / 2, (h - clientRect.bottom) / 2, clientRect.right, clientRect.bottom);

  hdc = GetWindowDC(this.Handle);

  using (Graphics g = Graphics.FromHdc(hdc))
  {
    g.SetClip(clip, CombineMode.Exclude);

    g.FillRectangle(Brushes.SeaGreen, 0, 0, w, h);
  }

  ReleaseDC(this.Handle, hdc);
}

And with this in place, we now have a control that has a green border.

A default paint example, the non-client area is green
A default paint example, the non-client area is green

Sidequest - Regions

In the above code, I calculated the clip region manually. However, Windows does provide a paint region as part of the message. The wParam parameter is a handle to an update region. This doesn't always seem to be the case - the first call always seems to be 1 which isn't a valid handle. When it isn't 1, you can use Region.FromHrgn to convert this handle into a managed object, leaving you with code something similar to the below.

While trying to work find out what the seemly undocumented 1 meant, I came across this Stack Overflow answer and in turn this MSDN post which first made me realise why the calls to GetDCEx were failing but also made me realise I should probably ignore that parameter and continue doing it the way I was. As a bonus it also pointed me in the direction of the MapWindowPoints call which may be the missing piece I needed for offsetting the client rectangle, something to investigate another day now though. (It also made me question if I really should be using WM_NCPAINT or if I should just do it all manually, but I'm halfway through the article now so I may as well finish it!).

csharp
// this works but needs more investigation and probably shouldn't be used
using (Graphics g = Graphics.FromHdc(hdc))
using (Region region = m.WParam != new IntPtr(1)
                        ? Region.FromHrgn(m.WParam)
                        : new Region(clip))
{
  g.SetClip(region, CombineMode.Exclude);

  g.FillRectangle(Brushes.SeaGreen, 0, 0, w, h);
}

Painting using the themes API

Although I could probably just use the built in Visual Styles, using the native theme API is a nice way of demonstrating the pure unmanaged approach.

It starts of similar to the previous code, except I manipulate the results of GetWindowRect to be client based, otherwise the painting would done at the wrong location. In this example, I'm using the themes for the EDIT control, e.g. a TextBox.

As I don't have a Graphics object to call SetClip on, I call ExcludeClipRect instead.

For the actual painting, first I get a handle to the theme via OpenThemeData. Next I check to see if any the theme is partially transparent via IsThemeBackgroundPartiallyTransparent and if so I draw the background via DrawThemeParentBackground. In this case it isn't transparent, but I suppose it is good practice to do these checks. After which I draw the theme background via DrawThemeBackground which will give me my nice themed borders. And to wrap it up, I close the handle I opened earlier using CloseThemeData.

csharp
const int EP_EDITTEXT = 1;
const int ETS_NORMAL = 1;
const int ETS_DISABLED = 4;

[DllImport("uxtheme.dll")]
static extern int CloseThemeData(IntPtr hTheme);

[DllImport("uxtheme.dll")]
static extern int DrawThemeBackground(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, ref RECT pRect, IntPtr pClipRect);

[DllImport("uxtheme.dll")]
static extern int DrawThemeParentBackground(IntPtr hWnd, IntPtr hdc, ref RECT pRect);

[DllImport("gdi32.dll")]
static extern int ExcludeClipRect(IntPtr hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

[DllImport("uxtheme.dll")]
static extern int IsThemeBackgroundPartiallyTransparent(IntPtr hTheme, int iPartId, int iStateId);

[DllImport("uxtheme.dll", CharSet = CharSet.Unicode)]
static extern IntPtr OpenThemeData(IntPtr hWnd, string classList);

private void WmNcPaint(ref Message m)
{
  int w;
  int h;
  IntPtr hdc;
  IntPtr hTheme;
  int partId;
  int stateId;

  GetClientRect(this.Handle, out RECT clientRect);
  GetWindowRect(this.Handle, out RECT windowRect);

  w = windowRect.right - windowRect.left;
  h = windowRect.bottom - windowRect.top;

  windowRect.right = w;
  windowRect.bottom = h;
  windowRect.left = 0;
  windowRect.top = 0;

  hdc = GetWindowDC(this.Handle);
  hTheme = OpenThemeData(this.Handle, "EDIT");
  partId = EP_EDITTEXT;

  stateId = this.Enabled
    ? ETS_NORMAL
    : ETS_DISABLED;

  ExcludeClipRect(hdc, (w - clientRect.right) / 2, (h - clientRect.bottom) / 2, clientRect.right, clientRect.bottom);

  if (IsThemeBackgroundPartiallyTransparent(hTheme, partId, stateId) != 0)
  {
    DrawThemeParentBackground(this.Handle, hdc, ref windowRect);
  }

  DrawThemeBackground(hTheme, hdc, partId, stateId, ref windowRect, IntPtr.Zero);

  CloseThemeData(hTheme);

  ReleaseDC(this.Handle, hdc);
}
An example of painting using the themes API
An example of painting using the themes API

Although it is probably much more concise to use the built-in VisualStyleRenderer with a Graphics instance than the above, this serves the purpose and will give us a control with a nice themed border.

Themes shouldn't be used blindly, they might be disabled by the operating system or by the current process. You should always check to see if themes are enabled (for example via Application.RenderWithVisualStyles) before attempting to render them, and fall back to another paint mode if not.

Bonus Chatter - the WM_NCCALCSIZE message

When I started this article, I intended to end it with the preceding section. However, given that themes don't have to follow expected sizing conventions I thought I ought not do a half job and should include some details on how you can define the size of the non-client area yourself.

The WM_NCCALCSIZE message is sent when the size and position of a window's client area must be calculated. If you are just relying on painting over standard borders without using themes then you may not need to use this message, but if you are using themes them it is probably better to handle it manually.

This message is slightly peculiar in my experience as it contains different data at different times. If wParam is TRUE, then lParam points to a NCCALCSIZE_PARAMS structure, otherwise it points to a RECT instead. This makes the code slightly more complicated, but not excessively.

In my testing, it seemed a RECT value only happened once when first created, thereafter a NCCALCSIZE_PARAMS value was always provided.

csharp
const int WM_NCCALCSIZE = 0x0083;

[StructLayout(LayoutKind.Sequential)]
struct NCCALCSIZE_PARAMS
{
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
  public RECT[] rgrc;
  public WINDOWPOS lppos;
}

[StructLayout(LayoutKind.Sequential)]
struct WINDOWPOS
{
  public IntPtr hwnd;
  public IntPtr hwndInsertAfter;
  public int x;
  public int y;
  public int cx;
  public int cy;
  public uint flags;
}

protected override void WndProc(ref Message m)
{
  if (m.Msg == WM_NCPAINT)
  {
    this.WmNcPaint(ref m);
  }
  else if (m.Msg == WM_NCCALCSIZE)
  {
    this.WmNcCalcSize(ref m);
  }
  else
  {
    base.WndProc(ref m);
  }
}

private void WmNcCalcSize(ref Message m)
{
  if (m.WParam == IntPtr.Zero)
  {
    RECT clientRect;

    clientRect = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));
    clientRect.left += leftOffset;
    clientRect.top += topOffset;
    clientRect.right -= rightOffset;
    clientRect.bottom -= bottomOffset;

    Marshal.StructureToPtr(clientRect, m.LParam, false);

    _clientRect = new Rectangle(leftOffset, topOffset, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
  }
  else
  {
    NCCALCSIZE_PARAMS parameters;
    RECT clientRect;

    parameters = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));

    clientRect = parameters.rgrc[0];
    clientRect.left += leftOffset;
    clientRect.top += topOffset;
    clientRect.right -= rightOffset;
    clientRect.bottom -= bottomOffset;

    parameters.rgrc[0] = clientRect;
    Marshal.StructureToPtr(parameters, m.LParam, false);

    _clientRect = new Rectangle(leftOffset, topOffset, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
  }
}

The first thing I do is check if wParam is IntPtr.Zero, and if so I extract the RECT structure from lParam using Marshal.PtrToStructure. I then adjust this rectangle accordingly by increasing left and top, and reducing right and bottom to account for how large I want the client area to be. I then store the modified rectangle back into lParam using Marshal.StructureToPtr. Finally, I also store the new rectangle for future use in painting.

If wParam is non-zero, I instead extract a NCCALCSIZE_PARAMS structure from lParam. This has two members, a WINDOWPOS describing the window position and an array containing 3 RECT values. The first rectangle in this array is the rectangle is the one we want to modify as per the previous paragraph. Once we've replaced the first value in the array with our modified version, we store the entire structure back into lParam, again using Marshal.StructureToPtr.

When painting in response to WM_NCPAINT, I use the _clientRectangle value captured earlier to define the clipping region.

Using WM_NCCALCSIZE to specify a non-equidistant client rectangle
Using WM_NCCALCSIZE to specify a non-equidistant client rectangle

Processing this message in order to handle visual styles is reasonably straight forward - as with painting, we need to open a DC, open a theme, and then use GetThemeBackgroundContentRect to get the client rectangle instead of calculating it ourselves.

csharp
[DllImport("uxtheme.dll")]
extern static int GetThemeBackgroundContentRect(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, ref RECT pBoundingRect, out RECT pContentRect);

private void WmNcCalcSize(ref Message m)
{
  IntPtr hdc;
  IntPtr hTheme;
  int partId;
  int stateId;

  hdc = GetWindowDC(this.Handle);
  hTheme = OpenThemeData(this.Handle, "EDIT");
  partId = EP_EDITTEXT;

  stateId = this.Enabled
    ? ETS_NORMAL
    : ETS_DISABLED;

  if (m.WParam == IntPtr.Zero)
  {
    RECT clientRect;

    clientRect = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));

    GetThemeBackgroundContentRect(hTheme, hdc, partId, stateId, ref clientRect, out RECT adjustedClientRect);

    Marshal.StructureToPtr(adjustedClientRect, m.LParam, false);

    _clientRect = new Rectangle(adjustedClientRect.left - clientRect.left, adjustedClientRect.top - clientRect.top, (adjustedClientRect.right - adjustedClientRect.left) - (adjustedClientRect.left - clientRect.left), (adjustedClientRect.bottom - adjustedClientRect.top) - (adjustedClientRect.top - clientRect.top));
  }
  else
  {
    NCCALCSIZE_PARAMS parameters;
    RECT clientRect;

    parameters = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));

    clientRect = parameters.rgrc[0];

    GetThemeBackgroundContentRect(hTheme, hdc, partId, stateId, ref clientRect, out RECT adjustedClientRect);

    parameters.rgrc[0] = adjustedClientRect;
    Marshal.StructureToPtr(parameters, m.LParam, false);

    _clientRect = new Rectangle(adjustedClientRect.left - clientRect.left, adjustedClientRect.top - clientRect.top, (adjustedClientRect.right - adjustedClientRect.left) - (adjustedClientRect.left - clientRect.left), (adjustedClientRect.bottom - adjustedClientRect.top) - (adjustedClientRect.top - clientRect.top));
  }

  CloseThemeData(hTheme);

  ReleaseDC(this.Handle, hdc);
}
Although the border style is set to the 3D style that is two pixels in size, the painted area is only a single pixel, matching the theme setting
Although the border style is set to the 3D style that is two pixels in size, the painted area is only a single pixel, matching the theme setting

Bonus Chatter - Forcing a redraw of the non-client area

If you use this approach, you will find the WM_NCPAINT message isn't called all that often - you'll have a lot more WM_PAINT messages for painting the actual client area. But what happens if you need to refresh the non-client area? For example, you might have design time properties that control the appearance, or perhaps at runtime you want to change the border when the control has focus.

Built in methods like Control.Invalidate or Control.Refresh won't have any impact as they only invalidate the client area.

The solution is to use the RedrawWindow API, which allows us to control which aspects of the window are refreshed. By using the RDW_FRAME flag, we tell Windows to send a WM_NCPAINT message as appropriate, and the RDW_INVALIDATE flag to repaint the window. By not including a RECT or HRGN describing the area to repaint, it will paint the full window.

It would probably be better to try to define a region that includes the non-client area and excludes the client area, but this is an area I haven't touched for some time (if ever) so I'm fuzzy on the details - I may post a follow up if I find it becomes necessary.

csharp
const int RDW_FRAME = 0x400;
const int RDW_INVALIDATE = 0x1;

[DllImport("user32.dll")]
static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, int flags);

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

protected override void OnEnter(EventArgs e)
{
  base.OnEnter(e);

  this.InvalidateAll();
}

protected override void OnLeave(EventArgs e)
{
  base.OnLeave(e);

  this.InvalidateAll();
}

Closing thoughts

This article turned out a little longer than I was expecting. As is often the case, I wrote the article in tandem with creating the demonstration and jumped back and forth whilst exploring different ideas or encountering issues. As a result of this it's possible that there are errors in the code embedded within the article or with the article content itself. If you spot any, please let me know!

A sample project can be downloaded from our GitHub page.

Would you follow this approach or would you do something different?


  1. The reason this call was failing appears to be two-fold. Firstly, I was passing in an invalid HRGN whenever wParam was 1. In addition, when calling in response to WM_NCPAINT apparently you need to use an undefined DCX_USESTYLE (0x00010000) flag too.

  2. Potentially we can use MapWindowPoints for this, but at the time of writing this article I have not investigated this further.


Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Comments

We'll never share your email with anyone else Styling with Markdown is supported