25.2 A ''CDC'' is like a cranky six-legged ant

MFC formulates graphics in terms of a CDC class which encapsulates a handle to a device context. A device context is simply something that accepts graphics calls. A CDC can correspond to an onscreen window, to a printer, or to a region in memory that's been tailored so as to behave like a virtual device context. The CDC class has one primary member, an old-style Windows HDC object holding a handle. The CDC class's state also includes certain active tool objects. In addition, the CDC class has a wide range of graphics methods.

For the purposes of this section, let's think of a CDC as a cranky six-legged ant. If you anger a CDC, it has the ability to kick up such a fuss that Windows will crash. (The author is attracted to the ant analogy because he once wrote a novel, The Hacker and the Ants, about ants in cyberspace.)

Each CDC ant is born with one each of six kinds of graphics tools which are instances of the classes derived from an abstract base class called GDIObject. The six tools are a CPen, a CBrush, a CBitmap, a CPalette, a CFont, and a clipping CRegion. Each of these graphics tool classes is a child of the GDIObject class. The CDC holds one of these in each of its six legs. It has a special pen-holding leg, a brush-holding leg, a bitmap-holding leg, and so on.

When a CDC is created, it is clutching default GDIObject tools in its six legs: a black solid width-one pen, a white brush, a default empty bitmap, an empty palette, a standard Windows font, and an empty clipping region.

These default GDIObject tools are so-called stock objects. Two common examples of stock objects are the fill brush and the drawing pen obtained as, respectively, cbrush.CreateStockObject(WHITE_BRUSH) and cpen. CreateStockObject(BLACK_PEN).

A CDC is very possessive of its six tools. If you take one of them away, it spreads the alarm and Windows comes running to punish you! The CDC isn't particular about which tools it has, just so long as it has some tools. It just has to have one each of the six kinds of tool. The CDC can only hold one of each kind of tool. If you hand it a bitmap, it'll take the new bitmap with its 'bitmap leg,' but it'll have to drop the bitmap that it was holding before. So if we have a tool that we want to give the CDC ant for a while and then take back, there are two approaches we can use.

  • Approach one: When we hand the CDC ant the good tool it will drop its old tool of the same type. So we just pick up the tool it dropped, and save it for giving back to the ant later on, when we need to get our good tool back.

  • Approach two: Kill the ant before it can shriek. It drops all the tools it was holding and you can do whatever you want with them.

Generally we're going to use the first approach. This is because we will do almost all of our graphics calls inside of CView::OnDraw(CDC* pDC) methods. When the OnDraw method exits, the CDC* object is supposed to be still good; we don't have the option of killing it. Any temporary tool object that we create inside of OnDraw is going to be destroyed by its destructor at the termination of OnDraw ; we need to make sure that the pDC isn't holding onto any of these soon-to-be-destroyed objects.

There's another reason why it's so important for us to be able to take tools back from the CDC* and destroy them. This has to do with the fact that Windows can only keep track of a limited number of GDIObject instances, and if you create too many of them the program will start acting badly. So the two key rules to remember are this:

  • Don't delete GDIObjects while they are selected into a CDC .

  • Delete all GDIObjects you create.

As we hinted at just above, the second of these rules will very often happen automatically, in the case that our tool is a local variable inside some function. This is because when a locally declared GDIObject instance goes out of scope it gets automatically deleted.

Whenever we want to draw graphics (or write text), we do this by using a method of the CDC class, using either a CDC* pointer or a CDC object to call the method. A properly designed Windows program normally does all of its graphics calls from inside the CView::OnDraw(CDC* pDC) function, in which case pDC is the CDC* object that makes the graphics calls.

One exception to this rule is that sometimes we use special memory device context CDC objects which can also make some graphics calls. More about this in Section 25.5.

We should also point out that if you really want to, you can get a CDC* and write some graphics from inside a CView method other than OnDraw. In this case you can get a CDC* by calling GetDC(), and you then need to free up this CDC with a call to ReleaseDC(CDC* pDC). But let's repeat again that normally all of your graphics writing should take place inside OnDraw. The reason is that in this way you're able to make sure that your window's image is persistent.

Let's go over the Windows sandwich steps that we use in order to use a graphics tool such as a CPen, a CBrush, a CBitmap, a CFont.

  • Create the GDIObject. We can do this in one step by using a constructor like in the line CBrush cbrush(RGB(255,0,0)). Or we can do it in two steps with lines like the following.

    CBrush cbrush; 

    Each kind of GDIObject has its own special constructors and initialization functions, such as CreatePen, CreateFontIndirect, and CreateCompatibleBitmap. Look in Help for the individual kind of object to find out more.

  • Have your CDC* call the SelectObject method to select the tool. This call requires a pointer to a GDIObject class as an argument and returns a pointer to a GDIObject of the same class type. Save the 'old' tool in a temporary variable.

  • Use your selected tool implicitly by having the CDC* make calls on its various graphical methods, such as Ellipse.

  • Unselect the tool from the CDC by doing a SelectObject on the 'old' tool.

  • Explicitly call the tool's DeleteObject method to destroy the tool. The Windows documentation implies that each kind of GDIObject destructor calls the DeleteObject method automatically when a tool goes out of scope, but this may not be true for every version of Visual Studio and the MFC. Do the DeleteObject yourself, or there is a real chance that your program will develop a 'resource leak.'

By the way, what does happen if DeleteObject doesn't get called for some GDIObject that you are repeatedly creating? The first few dozen or few hundred times this happens in a program, you won't notice any problem. But if the bug is inside a drawing function used in the animation loop of a program it can happen thousands of times. And then Windows runs out of the 'graphics handles' that it needs to create new pen and brush tools. Your calls to CreateBrush and the like start failing, and your pens and brushes always stay at their default values, which are, respectively, a thin black line and white filling. So if your colorful program suddenly turns black and white, this means you have a resource leak caused by the failure to call DeleteObject on some pen or brush that you are repeatedly creating.

Since an CDC always has some instance of each kind of tool pre-selected, it's customary to save a copy of this tool for reselecting back in when you want to 'unselect' the new tool before it gets destroyed by going out of scope. Say we have a CDC *pDC, and we want to draw a hollow circle with an edge that's int intedgewidth pixels thick and with color COLORREF edgecolor. Also assume that we have specified the center coordinates and radius as int intx, inty, intradius. We could write a block of code like this.

CPen cpen, *ppen_old; 
CBrush cbrush, *pbrush_old; 
cpen.CreatePen(PS_SOLID, intedgewidth, edgecolor); 
ppen_old = pDC->SelectObject(&cpen); 
pbrush_old = pDC->SelectObject(&cbrush); 
pDC->Ellipse(intx ? intradius, inty ? intradius, intx + intradius, 
    inty + intradius); 
pDC->SelectObject(ppen_old); //Need to unselect the brush before 
    deleting cpen.DeleteObject(); //Delete the pen. 
    /* We don't really need to do these next two lines, but its just 
    as well to be consistent in one's habits. The reason we don't need 
    these steps is that we don't actually need to delete the cbrush 
    because you don't need to delete stock objects. On the other hand, 
    it doesn't cause any problems if you do happen to delete a stock 
    object. And if I wanted to chain this code together with some 
    further code that puts a new brush into cbrush, I would indeed 
    need to have emptied it out like this so that I can indeed put 
    something new inside it. Bottom line: Never pass up an opportunity 
    to call DeleteObject! */ 
pDC->SelectObject(pbrush_old); //Need to unselect the brush before 
deleting cbrush.DeleteObject(); //Delete the brush. 

The trick is that whenever you call CDC::SelectObject, the operation returns the old tool or object of the appropriate type which was already in the CDC. Often as not this 'old' object will be a stock GDIObject, possibly with no real information in it (the default bitmap, for instance, is NULL). So why hang onto the 'old' object? Because there actually is no 'unselect' CDC function such as you would like to use in the 'Unselect the tool' step that we listed as the fourth bullet point in our description above. You accomplish an 'unselect' by selecting something else into the CDC.

Note that in order to have a circle or an ellipse drawn with an empty or 'transparent' interior, we need to select in a CBrush that's been set to the stock object NULL_BRUSH. By the same token, if you want to avoid outlining your shapes, you can select in a stock object NULL_PEN. Check the Help | Index on CreateStockObject to learn about the other kinds of stock objects.

    Part I: Software Engineering and Computer Games
    Part II: Software Engineering and Computer Games Reference