Click here to Skip to main content
Click here to Skip to main content

Custom Controls in Win32 API: The Painting

, 12 Dec 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
Understanding the basics of custom control painting and avoiding the trap of control flicker

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. It is not that simple a task as it sounds, especially if you consider that most applications support more than one particular Windows version, and that in the last 10 years, 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.

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 the 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 some 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 resizing?"

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 are 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:

  1. 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.

  2. 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.

  3. 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 and it would need immediate repaint once again then after.

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 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 letter function, EndPaint(), frees any resources taken by BeginPaint() (the device context), and it also validates the invalid region, i.e., the invalid region of the control is 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 will pay 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 resizes 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 know the cause of the issue, it is quite easy to formulate hints to avoid 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).

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. When changing the control state, invalidate only the minimal required region of the control.

  6. 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.

  7. 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.)

  8. 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 look like a lot of work for a developer, it is very often a work which will be needed anyway for a fully featured control which implements, for example, things like hit testing (WM_HITTEST) and many 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 magical band called double buffering which can solve the flickering always (assuming erasing via WM_ERASEBKGND is suppressed). But there is a prize 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 code 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:

/* custom.h
 * (custom control interface)
 */
...
/* Style to request using double buffering. */
#define XXS_DOUBLEBUFFER         (0x0001)
...
/* custom.c
 * (custom control implementation)
 */

#include "custom.h"

static void
CustomPaint(HWND hwnd, HDC hDC, RECT* rcDirty, BOOL bErase)
{
    // ... Paint the control here.
}

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;

    // Create new bitmap-back device context, large as the dirty rectangle.
    hMemDC = CreateCompatibleDC(pPaintStruct->hdc);
    hBmp = CreateCompatibleBitmap(pPaintStruct->hdc, cx, cy);
    hOldBmp = SelectObject(hMemDC, hBmp);

    // Do the painting into the memory bitmap.
    OffsetViewportOrgEx(hMemDC, -(pPaintStruct->rcPaint.left),
                        -(pPaintStruct->rcPaint.top), &ptOldOrigin);
    CustomPaint(hwnd, hMemDC, &pPaintStruct->rcPaint, TRUE);
    SetViewportOrgEx(hMemDC, ptOldOrigin.x, ptOldOrigin.y, NULL);

    // Blit the bitmap into the screen. This is really fast operation and although
    // the CustomPaint() can be complex and slow there will be no flicker any more.
    BitBlt(pPaintStruct->hdc, pPaintStruct->rcPaint.left, pPaintStruct->rcPaint.top,
           cx, cy, hMemDC, 0, 0, SRCCOPY);

    // Clean up.
    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;  // Defer erasing into WM_PAINT

        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 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. Then we 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.

  • 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 needs to 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Martin Mitáš
Team Leader
Czech Republic Czech Republic
I'm a senior developer with more then 15 years of experience. I currently work for Avast Software. Although I professionally focus on unix, linux and other unix-like systems, I am also (in my leisure time) involved in some Windows-centric open-source development, including occasional patches into mingw-w64 project and being the main developer of mCtrl project.

Comments and Discussions

 
QuestionNice series (my 5) PinmemberH.Brydon6-Aug-13 12:47 
AnswerRe: Nice series (my 5) PinprofessionalMartin Mitáš6-Aug-13 13:07 
QuestionThis is a great series of articles! PinmentorPaul Watt23-Jul-13 5:56 
AnswerRe: This is a great series of articles! PinprofessionalMartin Mitáš23-Jul-13 6:08 
GeneralRe: This is a great series of articles! PinmentorPaul Watt2-Aug-13 21:20 
GeneralMy vote of 5 Pinmembergordon8822-Jul-13 15:56 
GeneralMy vote of 5 Pinmemberimagiro21-Jul-13 22:19 
GeneralRe: My vote of 5 PinprofessionalMartin Mitáš22-Jul-13 8:23 
GeneralWell done PinmvpEspen Harlinn17-Jul-13 0:56 
QuestionMany Thx Pinmember.dan.g.14-Jul-13 22:05 
AnswerRe: Many Thx PinprofessionalMartin Mitáš14-Jul-13 23:00 
GeneralRe: Many Thx Pinmember.dan.g.1-Sep-13 22:11 
GeneralRe: Many Thx PinprofessionalMartin Mitáš1-Sep-13 23:26 
GeneralRe: Many Thx [modified] Pinmember.dan.g.2-Sep-13 15:22 
GeneralRe: Many Thx Pinmember.dan.g.2-Sep-13 16:43 
GeneralRe: Many Thx PinprofessionalMartin Mitáš2-Sep-13 22:54 
GeneralRe: Many Thx [modified] PinprofessionalMartin Mitáš4-Sep-13 10:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 12 Dec 2014
Article Copyright 2013 by Martin Mitáš
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid