It's important to delete the GDI objects you create, but it's equally important to never delete a GDI object while it's selected into a device context. Code that attempts to paint with a deleted object is buggy code. The only reason it doesn't crash is that the Windows GDI is sprinkled with error-checking code to prevent such crashes from occurring.
Abiding by this rule isn't as easy as it sounds. The following OnPaint handler allows a brush to be deleted while it's selected into a device context. Can you figure out why?
void CMainWindow::OnPaint () { CPaintDC dc (this); CBrush brush (RGB (255, 0, 0)); dc.SelectObject (&brush); dc.Ellipse (0, 0, 200, 100); } |
Here's the problem. A CPaintDC object and a CBrush object are created on the stack. Since the CBrush is created second, its destructor gets called first. Consequently, the associated GDI brush is deleted before dc goes out of scope. You could fix this by creating the brush first and the DC second, but code whose robustness relies on stack variables being created in a particular order is bad code indeed. As far as maintainability goes, it's a nightmare.
The solution is to select the CBrush out of the device context before the CPaintDC object goes out of scope. There is no UnselectObject function, but you can select an object out of a device context by selecting in another object. Most Windows programmers make it a practice to save the pointer returned by the first call to SelectObject for each object type and then use that pointer to reselect the default object. An equally viable approach is to select stock objects into the device context to replace the objects that are currently selected in. The first of these two methods is illustrated by the following code:
|
The second method works like this:
|
The big question is why this is necessary. The simple truth is that it's not. In modern versions of Windows, there's no harm in allowing a GDI object to be deleted a split second before a device context is released, especially if you're absolutely sure that no drawing will be done in the interim. Still, cleaning up a device context by deselecting the GDI objects you selected in is a common practice in Windows programming. It's also considered good form, so it's something I'll do throughout this book.
Incidentally, GDI objects are occasionally created on the heap, like this:
CPen* pPen = new CPen (PS_SOLID, 1, RGB (255, 0, 0)); CPen* pOldPen = dc.SelectObject (pPen); |
At some point, the pen must be selected out of the device context and deleted. The code to do it might look like this:
dc.SelectObject (pOldPen); delete pPen; |
Since the SelectObject function returns a pointer to the object selected out of the device context, it might be tempting to try to deselect the pen and delete it in one step:
delete dc.SelectObject (pOldPen); |
But don't do this. It works fine with pens, but it might not work with brushes. Why? Because if you create two identical CBrushes, 32-bit Windows conserves memory by creating just one GDI brush and you'll wind up with two CBrush pointers that reference the same HBRUSH. (An HBRUSH is a handle that uniquely identifies a GDI brush, just as an HWND identifies a window and an HDC identifies a device context. A CBrush wraps an HBRUSH and stores the HBRUSH handle in its m_hObject data member.) Because CDC::SelectObject uses an internal table maintained by MFC to convert the HBRUSH handle returned by SelectObject to a CBrush pointer and because that table assumes a one-to-one mapping between HBRUSHes and CBrushes, the CBrush pointer you get back might not match the CBrush pointer returned by new. Be sure you pass delete the pointer returned by new. Then both the GDI object and the C++ object will be properly destroyed.