Flicker-Free Drawing Using Bounds Accumulation

Friday Aug 15th 2003 by Sjaak Priester
Share:

Use the versatile QBufferDC class for double-buffered drawing to the screen.

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

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved