Monday, October 19, 2009

25.5 A memory-based device context



[ Team LiB ]










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 xint yint nWidthint nHeightCDC* pSrcDCint xSrc,
int ySrcint 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 xint yint nWidthint nHeightint 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.







    [ Team LiB ]



    No comments:

    Post a Comment