Articles in this Series
Introduction
In the previous article, we learned the very basics of custom control implementation. Today, we take a closer look at control painting. This topic is quite important because successful controls must look nice and they must fit into the Windows environment. This task is not as simple as it sounds, especially if you consider that most applications support more than one particular Windows version, and that in the last 10 years, almost every Windows version changes the default visual appearance of its controls.
Multiple Painting APIs
Over time, Microsoft has provided many APIs for 2D graphics and painting. GDI (GDI32.DLL) is available since ancient times, and can be used everywhere. GDI+ (GDIPLUS.DLL) is part of Windows XP and newer (but a redistributable version of it can be downloaded from the Microsoft website). Direct2D is the newest and it is available only on Windows 7 and newer.
In general, the newer APIs are capable of better graphics (e.g., support for anti-aliasing, alpha channels, etc.) and have better performance characteristics (when used in the right way) as they can offload many tasks to the graphics card, while GDI operates mainly on the main system memory and on the CPU.
However, we will mainly stick with GDI, in this article as well as the following ones. For custom controls implementation it is usually sufficient, it works everywhere, and last but not least, the paint-related messages a control receives are GDI-centric due to historical reasons. Of course, using the newer painting APIs is possible but it is not the subject of our interest.
All that said, this article is not about GDI painting. There are plenty of resources on the net on that. On this site, the topic is quite thoroughly covered by Paul Watt:
Hello World
The preceding article already provided a code for a trivial control which was, of course, also capable of painting itself. Let's take a look at the painting code from that example once again. When the control's window procedure gets the message WM_PAINT
, it was just calling our function, CustomPaint()
:
static void
CustomPaint(HWND hwnd)
{
PAINTSTRUCT ps;
HDC hdc;
RECT rect;
GetClientRect(hwnd, &rect);
hdc = BeginPaint(hwnd, &ps);
SetTextColor(hdc, RGB(0,0,0));
SetBkMode(hdc, TRANSPARENT);
DrawText(hdc, _T("Hello World!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
}
static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg) {
case WM_PAINT:
CustomPaint(hwnd);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
The code sample already presents the very basic principle: Whenever the control needs painting, the system sends message WM_PAINT
. The handler (here the function CustomPaint()
) gets the device context handle (BeginPaint()
), then uses it to paint the control with GDI functions (here, it just draws the string "Hello world"
to the middle of the control), and then releases all resources with EndPaint()
.
So far, there is no issue. However for good painting of complex controls, we simply need better understanding of how things really work. Otherwise, you will probably find yourselves on some web forum, searching for an answer to the omnipresent question "How to prevent control flickering when it is being resized?"
Also, complex controls may have a need to paint in other times then when WM_PAINT
is received, or they may even have a need to paint outside its client area, either into the control's non-client area, or completely outside of the control's area.
The Big Picture
So how are the things around WM_PAINT
glued together? From a perspective of the control (or more generally, any window represented with a HWND
handle), there are several important messages, not just the WM_PAINT
, and also some other concepts tightly related to the control painting.
First of all, we have to answer the question which any attentive reader already asks: When does the system decide the control needs to be painted? For each HWND
, the system manages its window update region (also known as dirty region). The region describes part of the window, whose content is invalid and needs repainting. When the region is not empty, the system knows the control (actually its part as defined by the region) needs repainting and it will eventually send the control the WM_PAINT
when there is no other message in the message queue of the window's thread.
Normally, the system does not remember the contents of a window. For example, whenever a window is moved to the edge of the screen so part of it is behind the corner, or whenever it is obscured by another window, the actual contents of the window is lost. After the window becomes fully visible once again, the system adds the unveiled part of the window into the update region which then eventually leads to requesting its repainting.
The control may also need to repaint itself or its part because its state or the data it shows has changed. To do so, the control simply may call a Win32API
function to expand the update region InvalidateRgn()
or one of its more specific relatives. In most cases, the region to be invalidated has a rectangular shape, so in most cases, the function InvalidateRect()
is used instead.
I already noted that the repainting itself is also more complex than just sending the message WM_PAINT
:
-
First, the system asks the control to erase its background with the message WM_ERASEBKGND
. This may be as soon as the region is invalidated. The parameter wParam
is set to the window's device context, which can be used to paint something. However the painting, if used at all, should be very fast, and usually consists of only filling the area with some background brush, hence effectively erasing its background. If the control passes the message into DefWindowProc()
, this is exactly what happens. DefWindowProc()
simply gets the brush which was specified during window class registration (WNDCLASS::hbrBackground
) and fills the control with it. Have you ever seen a grayish window on your screen when your machine is very busy with some very CPU-intensive tasks and you bring open a new window or bring a window to the top of the Z-order? Well, this is the reason. The grayish window has already received the WM_ERASEBKGND
but not yet WM_PAINT
. The return value is also important, the message should return non-zero if it performs the erase, or zero if it does not. (DefWindowProc()
follows it and returns non-zero if it filled the control with the background brush, or zero if it did not because the WNDCLASS::hbrBackground
was set to NULL
.) We will see soon what it is good for.
-
If the update region also intersects the non-client area of the window, the system sends WM_NCPAINT
. For top-level windows, this message is responsible for painting window caption, the minimize and maximize buttons, and also the menu if the window has any. For child windows (i.e., controls), it is often used for painting a border and also scrollbars if the control supports them in a similar way as, for examples, the standard list-view and tree-view controls do. If the control just passes the message into DefWindowProc()
, it paints a border as specified by the window style WS_BORDER
and/or extended style WS_EX_CLIENTEDGE
and the scrollbars as set by SetScrollInfo()
. For today, we leave the WM_NCPAINT
aside and we will return to it in a future article.
-
Finally, the WM_PAINT
is sent. However, note that the system treats this message specially and it comes only after all other messages from the message queue are handled and it is empty. If you think about it, it makes sense. As long as there are other messages in the queue, they may change the state of the control, resulting in a need for yet another repainting.
The system assumes that a control uses BeginPaint()
and EndPaint()
when handling WM_PAINT
. Between the two calls, the application is expected to paint the whole invalid region of the control.
The first function, BeginPaint()
, initializes the pointed structure PAINTSTRUCT
and returns the device context (HDC
) to be painted on (the same handle as stored in the structure). Two members are often very useful when handling the painting: the member fErase
, which is set to TRUE
if the window has not been erased with WM_ERASEBKGND
(i.e., depending on the value that message has returned), and the member rcPaint
, which is the smallest rectangle enclosing the invalid region which needs repainting.
The latter function, EndPaint()
, frees any resources taken by BeginPaint()
(e.g. the device context), and it also validates the invalid region, i.e., the invalid region of the control becomes empty after this call.
The Flickering Problem
Often, when a non-experienced control developer creates his first custom control, he is quite happy with it until he uses it in an application which resizes it together with the main window it resides in. When running it, he is then surprised by its ugly flickering. It is a wide-spread problem, so we pay a special attention to it.
If you already understand how Windows manages the painting from the section above, you probably already can see the culprit:
- As the user is resizing the main application window, it gets
WM_SIZE
and in response it resizes the control. - The custom control's window procedure likely propagates its
WM_SIZE
into DefWindowProc()
. That function (reasonably) invalidates the whole control, assuming its window class was registered with CS_HREDRAW
and CS_VREDRAW
as it commonly is. - In response to invalidation, the control gets
WM_ERASEBKGND
which by default fills the whole window client according to the brush used when registering its class. - Very soon then-after the control gets
WM_PAINT
, painting itself fully in all its glory. - As the user continues to resize the window by dragging the mouse, all of this happens again and again, causing the effect of flickering, how the control goes from erased to fully painted state and back again in short succession.
Solving the Flickering
If you understand the cause of the issue, it is quite easy to formulate hints for avoiding it. But let's list them here anyway for the sake of completeness. I tried to list the hints in the order of their importance (which, of course, is a subject of my personal preference to some degree).
-
If possible, do not rely on WM_ERASEBKGND
, i.e., let it return non-zero without passing it into DefWindowProc()
. Often, WM_PAINT
can paint all the background of the dirty region and there is no need for such erasing.
-
If you need to handle erasing specially from the painting itself, it can often be optimized so that WM_ERASEBKGND
still does nothing but returns non-zero, and the handler of WM_PAINT
can do the "erasing" by also painting the areas not covered by the regular paint code when PAINTSTRUCT::fErase
is set.
-
To the reasonable degree, try hard to design the painting code so that it does not repaint the same pixels multiple times. E.g., to paint blue rect with red border, do not FillRect(red)
followed with FillRect(blue)
to repaint the inner contents from red to blue. Rather paint the red border as 4 smaller rectangle not overlapped with the blue contents.
-
For complex controls, the paint code may be often optimized to skip a lot of painting outside the invalid rectangle as specified by PAINTSTRUCT::rcPaint
, by proper organizing the control data.
-
When changing the control state, invalidate only the minimal required region of the control.
-
When the flickering still happens during resizing, consider to not using CS_HREDRAW
and CS_VREDRAW
. Instead, invalidate the relevant parts of the control in handling the WM_SIZE
manually. Often much smaller parts of the control may need repainting.
-
When the control supports scrollbars, and using them leads to flickering, make sure the scrolling code uses ScrollWindow()
function instead of invalidating whole control area. (Note we will cover the topic of scrolling in one of the sequel articles.)
-
Generally, the flickering effect is also reduced when the painting performs better. If your paint method does not rely on the system setting the clipping rectangle to the client rectangle of the control, you may use window class style CS_PARENTDC
when registering the control window class, leading to less work for BeginPaint()
.
Note that although some of the points above may seem like a lot of work for a developer, it is very often a work which will be needed to be done anyway for a fully featured control which implements, for example, features like hit testing (WM_HITTEST
). A lot of the code then may be reused. For example, consider a control which paints a table of some sort, each cell providing some interactive response when a user clicks in it. Then the code must be aware of the layout of the table, and the code computing the layout may be reused for the paint implementation and hit testing implementation.
In case everything above fails, there is also a magic wand called double buffering which can solve the flickering in all cases (assuming the erasing via WM_ERASEBKGND
is suppressed and delayed to WM_PAINT
). But there is some price to pay for using it: higher resource consumption, especially memory. Even if your control still has the flickering problem after all you did in some situation, I recommend to use double-buffering only if the application explicitly requested for that (e.g., by specifying a style for it) because often the application never allows the situation when the flickering occurs (e.g., the application never resizes the control).
Double Buffering
Double buffering is a painting technique based on the fact that you may paint the control into a memory-based bitmap instead of directly on the screen, and then, after all the complex painting is done, you may copy (blit) the whole bitmap on the screen in a brisk.
Let's present a sample code of how it may look like. We start with the trivial control implementation and from the previous part, add the double buffering code:
#define XXS_DOUBLEBUFFER (0x0001)
#include "custom.h"
static void
CustomPaint(HWND hwnd, HDC hDC, RECT* rcDirty, BOOL bErase)
{
}
static void
CustomDoubleBuffer(HWND hwnd, PAINTSTRUCT* pPaintStruct)
{
int cx = pPaintStruct->rcPaint.right - pPaintStruct->rcPaint.left;
int cy = pPaintStruct->rcPaint.bottom - pPaintStruct->rcPaint.top;
HDC hMemDC;
HBITMAP hBmp;
HBITMAP hOldBmp;
POINT ptOldOrigin;
hMemDC = CreateCompatibleDC(pPaintStruct->hdc);
hBmp = CreateCompatibleBitmap(pPaintStruct->hdc, cx, cy);
hOldBmp = SelectObject(hMemDC, hBmp);
OffsetViewportOrgEx(hMemDC, -(pPaintStruct->rcPaint.left),
-(pPaintStruct->rcPaint.top), &ptOldOrigin);
CustomPaint(hwnd, hMemDC, &pPaintStruct->rcPaint, TRUE);
SetViewportOrgEx(hMemDC, ptOldOrigin.x, ptOldOrigin.y, NULL);
BitBlt(pPaintStruct->hdc, pPaintStruct->rcPaint.left, pPaintStruct->rcPaint.top,
cx, cy, hMemDC, 0, 0, SRCCOPY);
SelectObject(hMemDC, hOldBmp);
DeleteObject(hBmp);
DeleteDC(hMemDC);
}
static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg) {
case WM_ERASEBKGND:
return FALSE;
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint(hwnd, &ps);
if(GetWindowLong(hwnd, GWL_STYLE) & XXS_DOUBLEBUFFER)
CustomDoubleBuffer(hwnd, &ps);
else
CustomPaint(hwnd, ps.hdc, &ps.rcPaint, ps.fErase);
EndPaint(hwnd, &ps);
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
As it can be seen, the prototype of the function CustomPaint()
has changed: It is now capable of painting the current control state to any device context, not just on the screen. The BeginPaint()
and EndPaint()
machinery has moved directly to the WM_PAINT
handler. When double buffering is enabled (the control has the style XXS_DOUBLEBUFFER
), the function CustomDoubleBuffer()
is called to paint the control onto a temporary bitmap, which is then copied on the device context retrieved by BeginPaint()
.
A few other things are worthy of any note as the code is quite self-explanatory:
-
When using the double buffering, we always pass bErase
as TRUE
because, obviously, when double-buffered, the in-memory bitmap must be painted completely from scratch.
-
As a small optimization, instead of allocating a bitmap for the complete control's client area, we allocate only as small as needed for the dirty region. However then we need to compensate the change in coordinates between the control's top left corner and the dirty rect's top left corner by arranging the proper view-port origin temporarily. We use the function OffsetViewportOrgEx()
for the purpose.
-
The control creates and destroys the bitmap each time WM_PAINT
is received. It is a potentially costly operation so this could be further optimized by some appropriate caching strategy of the bitmap for reuse. However, as the bitmap is potentially quite large and memory-hungry, you should not hold it all the time. In the real world, the control gets many WM_PAINT
messages in a short period of time when the user is using it, or none for longer periods of time when he is not (e.g., when the application window is minimized) so the cache should be a bit smart. Something like that would make the example much longer and I believe the reader can take this as an opportunity for a small exercise of his own creativity.
(Again, an example Visual Studio 2010 project is attached. It is a slightly enhanced and more complete version of the code presented here.)
Message WM_PRINTCLIENT
There is yet another message worth mentioning, related to painting. The controls aspiring on a position of good citizen in the Windows environment should also support the message WM_PRINTCLIENT
. In short, this message asks the control to paint itself into the provided device context (via WPARAM
). Printing in Windows, for example, takes use of this. Various tools like screen zooming or thumbnailing utilities may use it. Some coding tricks like painting on a glass window border are possible by using this message (but that is out of our topic, at least for today).
Note our new version of CustomPaint()
is exactly suitable for such purpose so implementing this message is a very straightforward task:
static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg) {
case WM_PRINTCLIENT:
{
RECT rc;
GetClientRect(hwnd, &rc);
CustomPaint(hwnd, (HDC) wParam, &rc, TRUE);
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
Next Time: Visual Styles
I omitted talking about one API closely related to painting of controls. It is an API which provides support for visual themes (implemented by UXTHEME.DLL). As it was introduced in Windows XP, it is sometimes also referred as XP theming. In these days, the controls simply must be theme-aware if they want to fit into the Windows look and feel.
Though, it is quite a complex topic of its own, so the next time a whole article will be dedicated to it.