Flicker-Free Displays Using an Off-Screen DC

 

Herman Rodent
Microsoft Developer Network Technology Group

Created: April 5, 1993

Click to open or copy the files in the Flicker sample application for this technical article.

Abstract

This article describes a technique for drawing to a window device context (DC) in such a way that the screen does not flicker. The technique is very simple and easy to implement.

Introduction

I often see Microsoft® Windows™-based applications that maintain status information such as the current time in a small control window that flickers very annoyingly each time it's updated. All the standard Windows controls flicker if updated at frequent intervals. The solution to this problem is to implement a simple control yourself and use an off-screen DC to construct the image, which is then copied in total to the client area of the control window. The net result is a control that can be updated without any flicker.

Flicker, the sample application included with this article, has window procedures for two controls—one of these flickers; the other one doesn't. The application creates an instance of each of these controls and updates the window text ten times every second to show how one flickers and the other doesn't. The controls both support the system text color, window background color, and the WM_SETTEXT and WM_SETFONT messages.

A Control That Flickers

Let's start by looking at the window procedure of the control class that flickers. After that, we can go on to see how this is changed to prevent flickering. Here's the window procedure for the control:

LRESULT CALLBACK FlickerWndProc(HWND hWnd, UINT msg,
                                WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps;

    switch(msg) {
    case WM_SETTEXT:
        lstrcpy(szCaption, (LPSTR)lParam);
        InvalidateRect(hWnd, NULL, TRUE);
        break;

    case WM_SETFONT:
        hfnt = (HFONT) wParam;
        break;

    case WM_PAINT:
        BeginPaint(hWnd, &ps);
        Paint(hWnd, &ps);
        EndPaint(hWnd, &ps);
        break;

    default:
        return DefWindowProc(hWnd, msg, wParam, lParam);
        break;
    }
    return NULL;
}

The window supports WM_SETTEXT, WM_SETFONT, and WM_PAINT messages. When it receives a WM_SETTEXT message, the window copies the text to a local buffer and then invalidates the entire window to force the sending of a WM_PAINT message. When the window receives a WM_SETFONT message, it saves the font handle for use in its paint routine. Most of the work is done by the paint routine, which is shown here:

static void Paint(HWND hWnd, LPPAINTSTRUCT lpPS)
{
    RECT rc;
    HFONT hfntOld = NULL;

    GetClientRect(hWnd, &rc);
    SetBkMode(lpPS->hdc, TRANSPARENT);

    if (hfnt) {
        hfntOld = SelectObject(lpPS->hdc, hfnt);
    }

    SetTextColor(lpPS->hdc, GetSysColor(COLOR_WINDOWTEXT));

    DrawText(lpPS->hdc,
             szCaption,
             -1,
             &rc,
             DT_CENTER);

    if (hfntOld) {
        SelectObject(lpPS->hdc, hfntOld);
    }
}

At the start of the procedure, we get the client rectangle information that tells us where we can draw. The background mode is set to transparent, and if a WM_SETFONT message has provided a specific font handle, it is selected into the DC provided in the paint message. DrawText is then used to render the text into the DC and to restore the font, if it has been changed, to what it originally was.

What makes this window flicker when we update it frequently? The answer is that Windows asks the window procedure to repaint the window as a two-step process. First, it sends a WM_ERASEBKGND message and then a WM_PAINT message. The default handling for the WM_ERASEBKGND message is to fill the area with the current window background color. So the sequence of events is first to fill the area with solid color and then to draw the text on top. The net result of doing this frequently is that the window state alternates between its erased state and its drawn state—it flickers.

A Control That Doesn't Flicker

To prevent the control from flickering when we update it frequently, we need to make two changes to how the control handles messages. First, we need to prevent Windows from providing the default handling of WM_ERASEBKGND messages. Secondly, we need to handle WM_PAINT messages so that the background is painted with the window background color and so that the changes to the control's client area happen at once. Here's the new window procedure:

LRESULT CALLBACK NoFlickerWndProc(HWND hWnd, UINT msg,
                                  WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps;

    switch(msg) {
    case WM_SETTEXT:
        lstrcpy(szCaption, (LPSTR)lParam);
        InvalidateRect(hWnd, NULL, TRUE);
        break;

    case WM_SETFONT:
        hfnt = (HFONT) wParam;
        break;

    case WM_ERASEBKGND:
        return (LRESULT)1; // Say we handled it.

    case WM_PAINT:
        BeginPaint(hWnd, &ps);
        Paint(hWnd, &ps);
        EndPaint(hWnd, &ps);
        break;

    default:
        return DefWindowProc(hWnd, msg, wParam, lParam);
        break;
    }
    return NULL;
}

The only change here is that we now process the WM_ERASEBKGND message by returning a nonzero value, which tells Windows that we have taken care of it. This prevents the DefWindowProc function from performing the default action, which would normally erase the client area. The other changes are made in the paint routine shown here:

static void Paint(HWND hWnd, LPPAINTSTRUCT lpPS)
{
    RECT rc;
    HDC hdcMem;
    HBITMAP hbmMem, hbmOld;
    HBRUSH hbrBkGnd;
    HFONT hfntOld;

    //
    // Get the size of the client rectangle.
    //

    GetClientRect(hWnd, &rc);

    //
    // Create a compatible DC.
    //

    hdcMem = CreateCompatibleDC(lpPS->hdc);

    //
    // Create a bitmap big enough for our client rectangle.
    //

    hbmMem = CreateCompatibleBitmap(lpPS->hdc,
                                    rc.right-rc.left,
                                    rc.bottom-rc.top);

    //
    // Select the bitmap into the off-screen DC.
    //

    hbmOld = SelectObject(hdcMem, hbmMem);

    //
    // Erase the background.
    //

    hbrBkGnd = CreateSolidBrush(GetSysColor(COLOR_WINDOW));
    FillRect(hdcMem, &rc, hbrBkGnd);
    DeleteObject(hbrBkGnd);

    //
    // Select the font.
    //

    if (hfnt) {
        hfntOld = SelectObject(hdcMem, hfnt);
    }

    //
    // Render the image into the offscreen DC.
    //

    SetBkMode(hdcMem, TRANSPARENT);
    SetTextColor(hdcMem, GetSysColor(COLOR_WINDOWTEXT));
    DrawText(hdcMem,
             szCaption,
             -1,
             &rc,
             DT_CENTER);

    if (hfntOld) {
        SelectObject(hdcMem, hfntOld);
    }

    //
    // Blt the changes to the screen DC.
    //

    BitBlt(lpPS->hdc,
           rc.left, rc.top,
           rc.right-rc.left, rc.bottom-rc.top,
           hdcMem,
           0, 0,
           SRCCOPY);

    //
    // Done with off-screen bitmap and DC.
    //

    SelectObject(hdcMem, hbmOld);
    DeleteObject(hbmMem);
    DeleteDC(hdcMem);

}

As you can see, quite a lot has changed here. The new window procedure obtains the client rectangle coordinates as before and then creates a memory DC that is compatible with the DC supplied in the paint message. We can't draw to this DC with any effect yet because, by default, it has only a 1-by-1 monochrome bitmap associated with it. The next thing to do, therefore, is create a bitmap that has the same color organization as the DC we want to draw in and select that bitmap into the memory DC. Now we can draw on the memory DC, which will affect the bits in the bitmap selected in it. In other words, the bitmap will save the image of whatever we draw on the DC.

When the bitmap was created, nothing was done to any of the pixels in it, and consequently, we need to set them all to a known state. To do this, we fill the area with background color by creating a brush of that color and then using the FillRect function to do the work.

Once this has been done, we can perform the same operations that were performed in the flickering version: Set the background mode, select the font to use, and call DrawText to generate the text image. At this point the memory DC has the image of what we want to appear in the client area of the control window, and we can transfer the image from the memory DC to the window DC by calling BitBlt. This copies the entire rectangle (background and text image) in one go—and that's what stops you from seeing any flickering effect.

Once the blt is done, we can free the memory bitmap and DC, and exit the paint procedure.

Further Improvements

If performance is an issue for your application, you can do further optimizations.

Maintaining a Permanent Bitmap

Instead of creating a compatible bitmap each time the window is painted, you might consider creating the bitmap when the window is created and storing the handle to it in an extra word of window information created by setting the cbWndExtra field to sizeof(HBITMAP) when the window class is registered. At WM_CREATE time, allocate local memory for the bitmap and save the handle to it in the extra window word.

In processing the WM_PAINT message, you can quickly get the bitmap handle and proceed as before. This ensures that you cannot run out of local memory at paint time trying to allocate the bitmap, but does mean that you must also destroy and recreate the bitmap each time the window size changes.

Updating Only the Out-of-Date Regions

You may commonly find part of a large window area overlapped by another window, and consequently, much of what you might be painting is hidden. The PAINTSTRUCT information in the WM_PAINT message contains a clipping rectangle. You can use this information to work out what bits of your image need to be repainted and thus avoid repainting the entire thing needlessly. This is most effective when the paint procedure has a lot of work to do and least effective if the paint procedure has something simple like a blt to do.

Summary

Using an off-screen DC is simple to do and greatly enhances the visual impression of controls that have constantly changing data.

Show: