25.5 A memory-based device context


25.5 A memory-based device context

In order to keep our rapidly animated Windows displays from flickering , we use the cMemoryDC objects as virtual windows, or memory bitmaps. It is not a standard MFC class like CPoint , nor is it a well-known kind of user -written class like cVector . cMemoryDC is a special memory device context class that the author implemented here in order to make Windows programming easier.

The cMemoryDC class is a child of the standard CDC class. This means that we can write to a cMemoryDC with the same graphics methods that the CDC class uses to put graphics into an onscreen window or onto a printer page. And because cMemoryDC is a kind of CDC , we can use the powerful CDC::BitBlt method to do extremely fast copying from our cMemoryDC to a window-based CDC .

What makes the cMemoryDC special is that instead of being based on some actual device, its writing area is a bitmap that lives in memory. Ten or fifteen years ago, the cMemoryDC approach was not practical because it requires a goodly amount of RAM for the memory bitmap “ and 10 or 15 years ago, computers didn't have very much RAM. Although nowadays using a memory bitmap is becoming a fairly standard kind of trick for professional programmers, many books on Windows programming don't mention it. One exception is MFC Programming from the Ground Up , by Herbert Schildt (Osborne, 1996). Though Schildt does not encapsulate the memory-bitmap-plus-HDC technique and make a class of it as we do here, he does informally refer to the assemblage as a virtual window . Charles Petzold's classic book Programming Windows 95 (Microsoft Press, 1996) also has some discussion of the idea under the name of memory device context .

Our Pop program uses a cMemoryDC in order to achieve smooth-looking graphics updates. The idea is to assemble the pieces of the new image in an offscreen memory bitmap. We paint the CPopView's _cMemDC with our background color , and then we use the cPolygon::draw method to paste images of the polygons on top of the background. Once all this is done, _cMemDC uses its copyTo method to put the fresh image onto the visible screen.

Why not just do all this directly on the pDC that represents the onscreen window? Because it would be ugly and distracting to erase the critter bitmap images on the screen and then redraw them. You'd see drastic flicker. It's much nicer to use the _cMemDC as an offscreen drawing pad.

Another reason to use a cMemoryDC is that drawing to a memory bitmap device like _cMemDC is often much faster than drawing to an onscreen bitmap like the one embodied in the actual window's CDC . The reason is that when you write to screen, you have to go through your computer's graphics card, which is usually the biggest speed bottleneck of any graphics program. When we write to a memory bitmap, we're just letting our screaming-fast CPU processor chip move bytes around in our RAM.

So when you want to achieve a real time animation effect, the only way to go is to get your next frame ready in a cMemoryDC . The reasons are, again, that (a) it is prettier than having your users see the picture being assembled , and (b) it is faster.

You should use a cMemoryDC not only in animation programs but in any program at all where you are writing a bunch of graphics to the screen in OnDraw . The reason is that if you are writing a lot of graphics it takes some time, and if the user sees this happening it looks bad. The right way to do graphics is always to keep a cMemoryDC for your view, and inside your OnDraw put all the graphics into the cMemoryDC and then use the cMemoryDC::copyTo method to blast them into the user window.

Another use for cMemoryDC objects is to use one to hold an image of the background you want to use for your program. And, as we'll see below, there is a useful child class called cTransparentMemoryDC which is useful for holding small bitmaps to be used to represent movable objects such as game characters . A third trick is to use a large cTransparentMemoryDC as a foreground 'scrim' to put over your game pieces. But first we need to understand the basic use of a cMemoryDC .

The cMemoryDC class definition

Here's a partial, bare-bones listing of the cMemoryDC class definition. Because the author has worked with the cMemoryDC class for so long now, it's acquired a lot of bells and whistles, but we don't need to worry about those for now. See the memorydc.h code for a full listing.

 class cMemoryDC : public CDC  {  protected:      CBitmap _cBitmap;      COLORREF _blankcolor;      int _cx, _cy;  public:  //Constructors and destructor      cMemoryDC();      cMemoryDC(int nSize, COLORREF blankcol = RGB(255, 255, 255));      virtual ~cMemoryDC(); /* The destructor is declared to be virtual          because cMemoryDC has a child class cTransparentMemoryDC,          and the child's destructor is different. The methods that          differ between parent and child are also declared virtual. */  //Accessors      int cx(){return _cx;}      int cy(){return _cy;}  //Mutators      void clear();      void setBlankColor(COLORREF blankcol);  //Blt Methods      virtual void copyTo(CDC *pDC, const CRect &rect);  }; 

The size of a class object

How much size might a cMemoryDC object take up? One might think that maybe a class object had to carry around pointers to its methods, and maybe a class object's size is larger than the sum of its data field sizes. But this is wrong. The C++ compiler keeps the names of a class's methods straight in some global class-method pointer tables that it builds. So a class object isn't responsible for keeping track of its function pointers, and a class is no larger than a struct with the same data.

So now our question is: how many bytes are used up by a cMemoryDC object's data fields?

Well, as a child of the CDC class, a cMemoryDC inherits the CDC data fields which happen to be two ' HDC handles' called m_hDC and m_hAttribDC . (In all ordinary situations these two handles are the same, and we don't bother mentioning the second one.) Now a 'handle' is really just an int that the Windows operating system uses internally as a kind of half-baked pointer. So the size of the data inside a CDC is the same as the size of two integers. Since we're using 32-bit integers, this means four bytes per integer, so we have a total of eight bytes in a CDC .

Now we add in the size of the cMemoryDC's own data fields. How big is the CBitmap member? Like a CDC , a CBitmap is a ' shallow wrapper' around a Windows handle, in this case an ' HBITMAP handle' that, once again, is really just an int . So we pick up four bytes here. The COLORREF is also an int - sized object, so here's another four bytes. And we get eight more bytes out of the two int _cx, _cy .

Adding it up, we get 24 bytes in all.

Is a cMemoryDC a lightweight object in terms of memory demands? Yes and no. Yes, there's a small amount of data in the fields of the cMemoryDC . But no, it isn't really lightweight because a cMemoryDC's CBitmap field is likely to hold the handle of an HBITMAP which represents a pixel for pixel copy of your whole screen. And as we'll see in Size of a Bitmap section below, this can run into several Meg of data.

Declaration and construction of a cMemoryDC

Usually we will want to have cMemoryDC for each of our views. Even if two views are showing the same data, we will normally want to size the data display to be an appropriate fit for the view's size. An exception to this would be a program in which we wish to work with a graphic image of some fixed size and simply let the views show different pieces of the image; in this case we'd put the cMemoryDC inside the document class. But for the rest of this preliminary discussion we'll assume we're putting it inside the view.

You can have your cMemoryDC be a simple class member or you can have it be a pointer member. If it's a simple member you declare it with a line like cMemoryDC _cMemDC and you construct it with a line like _memDC(CMEMDC_FULLSCREEN, blankcolor); . The CMEMDC_FULLSCREEN parameter tells the constructor to make the cMemoryDC have as many pixels as a full-screen window. The blankcolor parameter tells the cMemoryDC to use a background color of blankcolor . If you just want a white background you can leave out the blankcolor argument.

You can also give a CMEMDC_ONEPIXEL argument into the first slot of the constructor if you want a cMemoryDC that's only one pixel big. The default constructor with no arguments also makes a single-pixel cMemoryDC , by the way. The purpose of the single-pixel cMemoryDC s is that they are used for loading bitmaps to be used for background images or for character icons. When a cMemoryDC loads a bitmap it can dynamically resize itself to the size of the bitmap.

It's worth looking at what happens inside a call to the constructor cMemoryDC(CMEMDC_FULLSCREEN, blankcol) so we get an idea of how the cMemoryDC works. Keep in mind that the members of a cMemoryDC which need initialization are the CBitmap _cBitmap , the COLORREF _blankcolor , and the two int _cx, _cy . As we already mentioned above, every CDC has an HDC m_hDC member, so as a child of the CDC class, a cMemoryDC has an HDC m_hDC which must be initialized as well. This initialization is accomplished implicitly by a call to CreateCompatibleDC .

Here are the principal code blocks that are executed in the cMemoryDC(CMEMDC_FULLSCREEN, blankcol) call, with a short comment on each one.

 CDC cDC_display;  cDC_display.CreateDC("DISPLAY", NULL, NULL, NULL); 

The purpose of the temporary CDC object cDC_display is to provide a role model object of what a CDC should be like in the runtime environment where this cMemoryDC is going to be used. The cDC_display gets destroyed when it goes out of scope at the end of the constructor's code.

 CreateCompatibleDC(&cDC_display); 

This is the line that initializes our cMemoryDC 's 'shallowly wrapped' HDC m_hDC field. The call makes our cMemoryDC be a CDC compatible with the screen. Each CDC will have selected into it a CBitmap bitmap object of a certain area. The CreateCompatibleDC method only makes our cMemoryDC compatible with the screen, but does not give it a bitmap as large as the screen. It's worth noting that in Windows, a 'normal' device context such as one that you get from a window never has any interesting bitmap associated with it. These 'normal' CDC always have an empty bitmap, and they don't use it at all. But the CreateCompatibleDC call is designed for creating memory-based device contexts. Although the default CDC constructor gives a CDC an empty CBitmap tool, the CreateCompatibleDC call gives the CDC a CBitmap that is one pixel big. It's not much, but it's something. It's up to us to replace this bitmap with one the size of the screen. So now we figure out the size of the screen.

 _cx = GetSystemMetrics(SM_CXFULLSCREEN);  _cy = GetSystemMetrics(SM_CYFULLSCREEN)  GetSystemMetrics(SM_CYMENU); 

Calling GetSystemMetrics with the SM_C?FULLSCREEN arguments gives the actual size of a full screen client window's screen measured in pixels. This assumes the window has a caption. We subtract off the region for the menu. Now we make the bitmap that we need.

 _cBitmap.CreateCompatibleBitmap(&cDC_display, _cx, _cy)) 

It is important to use the screen-based cDC_display as the argument to the CreateCompatibleBitmap call. (If you try and use your memory device context *this as the argument instead, you'll get a monochrome bitmap!) Another thing to note here is that CreateCompatibleBitmap is a kind of a memory allocation call, in that it's going to look for enough memory to hold a bitmap the size of _cx * _cy . Conceivably it might fail, so in our constructor code we're careful to check if this call is successful. Assuming it is, we now select the newly created _cBitmap into our cMemoryDC .

 SelectObject(&_cBitmap); 

And now our cMemoryDC is a screen-compatible CDC with an effective area as big as a full screen window. From now on, anything that we write to the cMemoryDC goes into the bitmap, and anything that we put into the bitmap appears in the cMemoryDC . (Although we don't mention this in the code printed here, we also need to do a DeleteObject on the single-pixel CBitmap that gets ' unselected ' by the SelectObject call.)

Size of a bitmap

How much RAM memory is a bitmap like _cBitmap going to use? Software engineers frequently have to talk about memory usage, and it's good to get fast at estimating it.

If your computer is running graphics in the lowest resolution mode, it has 640 x 480 pixels which you can round off in your head to 600 * 500 pixels, which is 300,000 or 300 K. If you are using the common 256 color mode, then you're using one byte of data per pixel. So that means 300 K bytes for a low resolution 256 color bitmap. 300 K is only a third of a Meg, which is no sweat compared to the many Megs of RAM that you're likely to have.

A lot of people use the 800 x 600 with 256 colors mode; here the bitmap is 480 K, or about half a Meg, still no big deal. If you go up to 24-bit color in a 'megapixel' mode of 1200 x 1000 you might end up needing 4 Meg per bitmap, which is still okay on most modern machines. But if you push your resolution high enough and use a lot of bitmaps, you may find a point where your RAM starts to suffer.

When Windows can't find enough RAM for a bitmap it will usually store the bitmap on the hard drive rather than returning an error from the CreateCompatibleBitmap call. This is good in that it means your program doesn't crash, but it's bad in that your program's behavior turns ugly once it starts using disk-based bitmaps.

The reason is that using a disk-based bitmap means lots of thrashing of your hard disk every time you uncover or resize a window onscreen. If your program switches to disk-based bitmaps you will notice a disturbing grinding sound from your hard drive every time you move your windows around.

But even with a low amount of RAM, you can almost always afford two or three screen-sized bitmaps. And you can have lots of small, icon-sized bitmaps. You'll only tend to find yourself running out of RAM for the cMemoryDC if you open up, say 20 or 30 different documents and/or views at once.

Writing to the cMemoryDC in OnDraw

The general principle of using the cMemoryDC class is that whenever we want to write something to the screen, we instead write it to our cMemoryDC _cMemDC , and then use the cMemoryDC::copyTo method to send the image to the screen. The exception is when we're printing; in this case we don't worry about flicker and we write directly to the print CDC (which will either be the printer or an image inside the Print Preview window). As usual we can use the CDC::IsPrinting() method to distinguish between the two cases, and our CView::OnDraw(CDC *pDC) code could look something like this.

 if (!pDC->IsPrinting()) //The standard onscreen window case  {      //Put code here to draw your image into the _cMemDC... And then:      _cMemDC.copyTo(pDC);  }  else //The Print or Print Preview case      //Put code here to draw your image directly to the pDC... 

When we are not involved in printing, we write to the screen in a two-step process. The first step is to assemble our image in the cMemoryDC , and the second step is to copy the cMemoryDC to the onscreen CDC .

The way we'll carry out our first step is that we'll write some kind of background image into our cMemoryDC , and then we'll write the images of our objects on top of it. In the case of the Pop program we use the simplest kind of background: we simply erase whatever was in the cMemoryDC and fill the image with the background color. This is encapsulated in our cMemoryDC::clear() method, which creates and selects a CBrush of color _blankcolor and then uses the PatBlt method to paint the whole cMemoryDC with the brush with the following line. (See the subsection below for information about PatBlt .)

 PatBlt(0, 0, _cx, _cy, PATCOPY); 

Once we've fixed our background, we put our graphics into the cMemoryDC .

And then we're ready for the second step of the OnDraw process, of copying the cMemoryDC to the screen. We do this in the line _cMemDC.copyTo(pDC) . This call to the cMemoryDC::copyTo function in turn calls the following line.

 pDC->BitBlt(0, 0, _cx, _cy, &_cMemDC, SRCCOPY); 

Note that in this BitBlt call, the 'target' pDC goes on the left and the 'source' &_cMemDC goes inside the BitBlt arguments. More about the BitBlt method is in the subsection below.

The BitBlt function

The CDC::BitBlt method is designed to move a rectangular block of pixel data from a source CDC to a target CDC . The CDC which calls the BitBlt method is the target; in effect, the caller CDC is saying 'copy a block of pixels to me.' You are allowed to specify the location and size of the target rectangle that you want to copy to, as well as the location of the source rectangle you're copying from. Since BitBlt does a one-pixel-to-one-pixel copy, the size of the source is the same as the size of the target. The prototype looks like this.


BOOL CDC::BitBlt(int   x ,   int   y ,   int   nWidth ,   int   nHeight ,   CDC*   pSrcDC ,   int   xSrc ,
int   ySrc ,   int   dwRop   );  

The arguments represent: the left upper corner and the horizontal and vertical extent of the target rectangle within the target HDC , the source CDC , the upper left corner of the source rectangle in the source CDC , and the write method. There are 14 write methods called ROP codes for 'Raster OPeration', where 'raster' is a word meaning an orderly grid, such as the one pixels are arranged in. The most natural ROP method is called SRCCOPY . The other ROP methods form various logical combinations of the source pixels, the target pixels, and the active brush-pattern pixels.

In general the time it takes for a BitBlt to execute is directly proportional to the number of pixels moved, and this 'pixel area' is proportional to the product of the linear dimensions of the window. In other words, if you make your window twice as big, your BitBlt will run four times as slow. This is why one so often sees things like onscreen video being shown in very small windows.

The PatBlt function

PatBlt is a special kind of BitBlt function that doesn't use a source CDC . Its prototype is like this.


BOOL PatBlt(int   x ,   int   y ,   int   nWidth ,   int   nHeight ,   int   dwRop   );  

PatBlt takes the CDC 's currently selected CBrush and uses it to write over a specified target rectangle. Here the most commonly used ROP code is PATCOPY , which means to copy the brush pattern or color.

Let's take another look at our call to _cMemDC.copyTo(pDC) which gets turned into pDC->BitBlt(0, 0, _cx, _cy, &_cMemDC, SRCCOPY) . The _cx and _cy are the size of a full screen, so mightn't this code be inefficient if our window is smaller than the screen? Actually it doesn't matter because the pDC that's fed into the OnDraw function has a clipping region set to the size of the 'damaged' rectangle that it needs to repaint. The BitBlt will actually only try and do the pixel copying for the points that lie within the pDC clipping rectangle.

In an animated program, at each step the entire window will need to be repainted, and that's how big the clipping rectangle will be. If all you've done is to uncover a small corner of the window in a non-animated program, then that corner will be the clipping rectangle.

Calling the OnDraw

There's one final thing to remember to do when you use a cMemoryDC inside your OnDraw function. You either need to use the Invalidate(FALSE) call instead of the Invalidate() , or, better, you need to override the OnEraseBkgnd to do nothing, like this.

 BOOL CPopView::OnEraseBkgnd(CDC* pDC)  {  /* We normally don't want to erase the background because our onDraw      will cover it up with the _cMemDC copyTo. If we did erase the      background, we'd get flicker. This is also true with OpenGL. */      return TRUE;          //Don't call baseclass method, CView::OnEraseBkgnd(pDC);  } 

The argument to the CView::Invalidate(BOOL eraseflag) method specifies whether or not you should call OnEraseBkgnd , which normally will erase the screen with a hidden Windows background brush before writing to the screen in the OnDraw function. When we are refreshing the screen with a BitBlt of a cMemoryDC , we don't need to erase it. The reason for this is that the copyTo(pDC) call is going to 'erase' the screen anyway; that is, it's going to cover the screen over with a copy of whatever image you've drawn into the cMemoryDC .

Now you might think it's harmless to go ahead and erase the screen anyway, but far from it. If you erase the screen, it's momentarily white, and your eye is going to pick up a flicker. And then all our work with the cMemoryDC is for nothing. But of course if we've overridden OnEraseBkgnd to do nothing, then calling it will do nothing. (If you didn't bother to override OnEraseBkgnd , you could partly avoid the flicker by feeding a FALSE argument into your Invalidate calls, but it turns out that resizing the screen would still call Invalidate(TRUE) and give you your flicker.)

Let's sum up the use of the cMemoryDC .

  • Declare a cMemoryDC _cMemDC member of your CView class.

  • Initialize _cMemDC with a call to cMemDC(CMEMDC_FULLSCREEN) in the CView constructor.

  • In CView::OnDraw draw graphics into the _cMemDC , possibly using the _pMemDC >clear() to erase the old _cMemDC image.

  • In CView::OnDraw use _cMemDC.copyTo(pDC) to copy the image to the onscreen CDC .

  • Override CView::OnEraseBkgnd(CDC *pDC) to do nothing at all but return TRUE; . If you forget this, your view will flicker when you call Invalidate() , which by default calls Invalidate(TRUE) . Also the view will flicker when you resize the window, as the resizing code automatically calls Invalidate(TRUE) , which forces a call to OnEraseBkgnd .



Software Engineering and Computer Games
Software Engineering and Computer Games
ISBN: B00406LVDU
EAN: N/A
Year: 2002
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net