Flicker-Free Drawing Using Bounds Accumulation

Environment: VC++ 6.0/7.0, Windows95 (but read on...) and later.

Off-Screen Drawing with QBufferDC

To avoid annoying screen flicker while drawing with GDI or GDI+, using an off-screen bitmap buffer is a well-known technique. Basically, you do your drawing to a bitmap in memory, and then copy ('bitblt') the result to the screen.

In applying this method, we need to know two things. First, we have to determine how big the off-screen bitmap should be. Second, we will want to know which part of the bitmap we have to copy to the screen.

The first problem is easy to solve. The device context maintains a clip box. It will not draw anything outside this rectangle. So, there is no point in making the off-screen bitmap bigger than the clip box.

You can get the clip box by using CDC's member function GetClipBox, which is defined as:

virtual int CDC::GetClipBox(LPRECT lpRect) const;

Here, lpRect points to a RECT structure of a CRect object that receives the dimensions of the clip box, and thus the (minimum) size of the off-screen bitmap.

The second problem seems harder, at first sight. So hard, in fact, that it is often not solved at all. Many implementations, such as Keith Rule's clever MFC class CMemDC, simply bitblt all of the bitmap to the screen, even the parts on which nothing has been drawn.

It would be more efficient to copy only the part of the bitmap on which some actual drawing has taken place. One solution would be to define a variable rectDirty, and update this after every graphic primitive. Something along these lines:

// ...

CRect rectSmall(100, 100, 150, 150);
dcBufferBitmap.Rectangle(rectSmall);
rectDirty |= rectSmall;

CRect rectBig(120, 120, 450, 450);
dcBufferBitmap.Rectangle(rectBig);
rectDirty |= rectBig;

// ...

After all drawing is done, rectDirty would contain the union of all graphic primitives' bounding rectangles, and therefore the part of the bitmap that should be bitblt'ed to the screen.

This method would have been very inconvenient, to say the least. We would have to intersperse our drawing code with updating code for rectDirty. On top of that, this code often would not be as simple as shown above—think of TextOut calls, for instance. This hardly seems worth the trouble.

Funny enough, the Windows device context comes with built-in resources for just the things we tried to do. It is called 'bounds accumulation'. It's not very well known and not often used—in fact, I couldn't find any reference to it, apart from the official documentation. Without doubt, one of the main reasons lies in the poor documentation Microsoft delivers on this subject. You more or less have to guess how it works. I did so, and I hope I guessed right.

Windows Bounds Accumulation

Normally, bounds accumulation is turned off. To turn it on, you have to use the CDC member function SetBoundsRect (or its SDK equivalent):

UINT CDC::SetBoundsRect(LPCRECT lpRectBounds, UINT flags);

You then do all your drawing. After that, you can retrieve the part to which actual drawing took place with:

UINT CDC::GetBoundsRect(LPRECT lpRectBounds, UINT flags);

In the 'Remarks' section of Microsoft's documentation it is stated that 'The drawing bounds are useful for invalidating bitmap caches', and that is almost all the clarification we get. After some experimentation, however, I think I know how to put these obscure functions to use.

First, call SetBoundsRect with lpRectBounds = NULL and the following flags. (The flag DCB_RESET is not mentioned in the MFC documentation, but it is in the Windows GDI docs.)

dcBufferBitmap.SetBoundsRect(NULL, DCB_ENABLE | DCB_ACCUMULATE |
                                   DCB_RESET);

Next, do your drawing like you used to. Behind its back, the device context keeps track of the rectangular area to which it is drawn.

Finally, prepare a CRect rcBounds and call GetBoundsRect:

dcBufferBitmap.GetBoundsRect(rcBounds, DCB_RESET);

After that, rcBounds will contain the 'dirty rectangle' in logical coordinates. Only this part of the buffer bitmap needs to be bitblt'ed to the screen.

QBufferDC

I put all this together in a small class, called QBufferDC. You use it in the same way as you would use CMemDC:

void CWhateverView::OnDraw(CDC* pDC)
{
  QBufferDC dcBuffer(pDC);

  dcBuffer.MoveTo...
  dcBuffer.LineTo...

  // ...other drawing code to dcBuffer...

}

That's all there is to it. If dcBuffer gets out of scope, its destructor automatically copies the result of all drawing code to the screen DC. It could hardly be simpler.

QBufferDC has some nice features:

  • It is compatible with all Windows mapping modes, even the user-defined ones (MM_ISOTROPIC and MM_ANISOTROPIC).
  • It handles MFC printing and print preview. In those situations, no buffer bitmap is used.
  • If no buffer bitmap can be created because of memory lack (highly unlikely, these days), it reverts to normal DC operation.
  • To avoid frequent creation and destruction of bitmaps, one static bitmap is maintained.
  • An optional second constructor parameter lets you define another 'ternary raster operation' (rop-code) than the default SRCCOPY. This is useful for rubber banding and other advanced applications.

The source code for QBufferDC is quite heavily commented. It compiles in Visual C++ 6.0 and 7.0.

Demonstration Project

QBufferDemo is a small demonstration of QBufferDC. It's a silly program, just showing some silly graphics. It starts up in a small window because the interesting part is to scroll it.

If Demo Mode is switched on (the default), the background color is slightly modified to a random off-white color. This makes it possible to see which part of the screen is updated. Scroll the window, or overlay it temporarely with another window, to see which parts are updated. Note that the area around the graphics stays white, indicating that QBufferDC is not involved. No 'empty' bitmap is copied there.

The source code for the demonstration project is also included.

The Windows 98 Issue

Although according to the Windows GDI documentation, bounds accumulation is available in Windows 98 (even in Windows 95), the bad news is that it doesn't work. There are no crashes or other nasty things, but GetBoundsRect simply returns the same rectangle as GetClipBox. The result is that QBufferDC operates like an 'ordinary' bitmap buffer DC, without bounds accumulation. I tested QBufferDC successfully in Windows 2000 and Windows XP.

It almost seems that Get/SetBoundsRect in Windows 98 are empty functions, not doing what is claimed in the documentation. I find that rather hard to believe. If anyone knows more about this, I'll be glad to hear it.

Of course, there is always the possibility that I made a stupid mistake...

Download

Download demo project and source - 48 Kb



Comments

  • strange way of doing!

    Posted by Legacy on 09/16/2003 12:00am

    Originally posted by: Ferdinand Oeinck

    If you want to redraw a window efficiently you only want to redraw the part of the window which was hidden just before. This depends on a user action. The list of these rectangles, which form a region, can be get from calls to ::GetUpdateRgn and ::GetRegionData. After that you could clip your graphics objects againts these clipping rectangles.

    In your code you use all your graphics objects every time. You calculate the boundingbox of these and redraw them all every time. Even when I changed the windowclass of your view to not containt CS_HREDRAW and CS_REDRAW.

    Conclusion your code will always flicker!

    Reply
  • The downloadable demo application flickers

    Posted by Legacy on 08/18/2003 12:00am

    Originally posted by: Kevin

    The person who says that "you need to handle the WM_ERASEBACKGROUND message" does not understand people's complaints. It is the demo application that still flickers. The demo application should be updated. Honestly, most people will just download the demo app and see if it fulfills their needs. If it does not, then they will not bother to try to use it. (And most people will not try to fix the demo app.)

    - Kevin

    • Well said...

      Posted by turkinz on 05/03/2004 06:45pm

      Such an absurd, like the flickering demo in a flicker free section, is just inconceivable. :)

      Reply
    Reply
  • What about flickering during resize?

    Posted by Legacy on 08/16/2003 12:00am

    Originally posted by: Michal Mecinski

    When the window is resized, the flickering still occurs,
    
    because MFC registers windows with the CS_HREDRAW and
    CS_VREDRAW styles. That causes the entire window to be
    redrawn, and because we still need WM_ERASEBKGND to do its
    job, the flickering is visible.

    I was trying to solve this by registering the view's class
    without those mentioned styles (and doing the same for
    the parent frame):

    BOOL CQBufferDemoView::PreCreateWindow(CREATESTRUCT& cs)
    {
    LPCSTR szClassName = "MyView";

    WNDCLASS wndcls;
    memset(&wndcls, 0, sizeof(WNDCLASS));
    wndcls.lpfnWndProc = ::DefWindowProc;
    wndcls.hInstance = AfxGetInstanceHandle();
    wndcls.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndcls.style = CS_DBLCLKS;
    wndcls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndcls.lpszClassName = szClassName;
    if (!AfxRegisterClass(&wndcls))
    return FALSE;

    cs.lpszClass = szClassName;

    cs.dwExStyle |= WS_EX_CLIENTEDGE;
    cs.style &= ~WS_BORDER;

    return TRUE;
    }

    (similar for CMainFrame::PreCreateWindow)

    However sometimes the window isn't updated properly
    and some white strips remain when you resize it.
    Sometimes it works, sometimes it doesn't. Maybe it's
    something with the CScrollView?

    Anyway, I still believe it's better to disable the
    WM_ERASEBKGND handler and blit the entire bitmap into
    the window's DC. Blitting is much faster than drawing
    complicated vector graphics, so this isn't noticable.
    Besides, BitBlt cooperates with update regions, and when
    you are scrolling the window, only the revealed part is
    copied anyway (if you handle update regions properly,
    and that's the case for CScrollView).

    Perhaps accumulated bounding is still a good idea for some
    uses, but disabling WM_ERASEBKGND and blitting the entire
    bitmap and relying on the update regions seems a better
    solution to me.

    regards,
    Michal

    Reply
  • Just what I was looking for. It works great. Thanks!

    Posted by Legacy on 08/15/2003 12:00am

    Originally posted by: Roger Dahl

    Just what I was looking for. It works great. Thanks!

    Reply
  • Well done, thanks.

    Posted by Legacy on 08/15/2003 12:00am

    Originally posted by: Hans Wedemeyer

    as per subject

    Reply
  • still flicks!

    Posted by Legacy on 08/15/2003 12:00am

    Originally posted by: Delfins

    subj.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Instead of only managing projects organizations do need to manage value! "Doing the right things" and "doing things right" are the essential ingredients for successful software and systems delivery. Unfortunately, with distributed delivery spanning multiple disciplines, geographies and time zones, many organizations struggle with teams working in silos, broken lines of communication, lack of collaboration, inadequate traceability, and poor project visibility. This often results in organizations "doing the …

  • You probably have several goals for your patient portal of choice. Is "community" one of them? With a bevy of vendors offering portal solutions, it can be challenging for a hospital to know where to start. Fortunately, YourCareCommunity helps ease the decision-making process. Read this white paper to learn more. "3 Ways Clinicians can Leverage a Patient Portal to Craft a Healthcare Community" is a published document owned by www.medhost.com

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds