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_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
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
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.
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 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
although that doesn't come up as often - a topic for another
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.
WM_NCPAINT message is sent to a window when its frame must
be painted. We can intercept this message via the
method of our control and then perform the desired painting.
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
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
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
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
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
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
The new painting implementation is a little longer, but more robust.
And with this in place, we now have a control that has a green border.
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
you can use
Region.FromHrgn to convert this handle into a
managed object, leaving you with code something similar to the
While trying to work find out what the seemly undocumented
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!).
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
As I don't have a
Graphics object to call
SetClip on, I call
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
and if so I draw the background via
this case it isn't transparent, but I suppose it is good
practice to do these checks. After which I draw the theme
which will give me my nice themed borders. And to wrap it up, I
close the handle I opened earlier using
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
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.
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.
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
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
RECTvalue only happened once when first created, thereafter a
NCCALCSIZE_PARAMSvalue was always provided.
The first thing I do is check if wParam is
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
structure from lParam. This has two members, a
describing the window position and an array containing 3
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
When painting in response to
WM_NCPAINT, I use the
_clientRectangle value captured earlier to define the clipping
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
to get the client rectangle instead of calculating it ourselves.
If you use this approach, you will find the
isn't called all that often - you'll have a lot more
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
won't have any impact as they only invalidate the client area.
The solution is to use the
which allows us to control which aspects of the window are
refreshed. By using the
RDW_FRAME flag, we tell Windows to
WM_NCPAINT message as appropriate, and the
RDW_INVALIDATE flag to repaint the window. By not including a
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.
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?
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_NCPAINTapparently you need to use an undefined
0x00010000) flag too.↩
Like what you're reading? Perhaps you like to buy us a coffee?