Saving Drawing Contexts |
|
There are some problems with maintaining resources for drawing, such as pens, brushes, etc. When you select an object into a DC, that object becomes undeletable. You might exit the context in which it was allocated, and its destructor is called, and DeleteObject is ultimately called on it, but, surprise!, it hasn't really gone away. Because it was selected into a DC, there is a flag saying it is inactive use, and therefore your DeleteObject can't work. I don't know the actual details of how this works internally, but I know the external manifestation. The pen, brush, etc. simply stays in the "GDI heap".
If you are working on one of those horrid 16-bit windows-kludge-on-top-of-MS-DOS systems like Windows 3.1, Windows 3.11, Win98, Win98, or WinME, (and if you think that Win9x/ME is a 32-bit OS, you have (a) not read the API documentation carefully (b) believed Microsoft's consumer-level documentation and thought it was technical documentation) there is a very limited system-wide pool out of which all pens and brushes are allocated, and eventually not even your desktop can allocate the resources it needs to draw. On a real, 32-bit operating system such as NT/2000/XP, the GDI heap is much larger, and is per-process, so eventually your program won't be able to draw, but the rest of the system will work fine, since you are only allowed to shoot yourself in the foot.
Here's a classic case:
void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(...); // parameters not shown dc.SelectObject(&f); dc.TextOut(...); // whatever... } // destructors called here...
Looks pretty good, right? Wrong. Look at what happens. The destructors are called when the context exits. This means that the DC will be freed (in the case of a CPaintDC this means that ::EndPaint will be called), and the destructor for the CFont will be called, which means that a ::DeleteObject will be called. (This has other implications, for example, if you are doing a CWnd::SetFont call, where the font has to have a lifetime beyond the lifetime of the variable; see my essay on this topic).
Strictly speaking, there is no specified order in which the destructors are known to be called. I checked the C++ standard, and although all sorts of issues are specified in order of execution of destructors, the order in which they are called for auto variables (that is, ordinary stack variables) seems to be unspecified. In practice, it appears to be in the inverse order of their declaration, that is, the font, which is declared after the DC, will be destroyed first, then the DC will be destroyed. This loses, because when the font is destroyed it is still selected into the font. (You may suspect that by declaring your fonts before the CPaintDC will solve this. I would consider this an egregious programming blunder. For one thing, I'm not sure that deleting the DC first properly sets the values in the font so that it knows it is no longer in a DC (it could be selected into several DCs). I would never attempt this.
The proper thing to do is to restore the state of the DC before destroying it. Typically, this is done by saving the contents of the DC when you do a SelectObject and then restoring them, for example,
void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(...); // parameters not shown CFont * oldfont = dc.SelectObject(&f); dc.TextOut(...); // whatever... dc.SelectObject(oldfont); } // destructors called here...
This will now work properly. When the destructor for the font is called, it is no longer in a DC, and it will be deleted.
In fact, the principle that hikers follow, "Leave gates as you found them", is important in DC management. When you write a function that takes an incoming DC, you have to make sure that every change you make is restored. Note that every operation that modifies a DC returns an object (or value) that can be used to restore the state of the DC (the MFC calls wrap certain values, such as HFONT values, in a corresponding "wrapper class", such as CFont, and return a pointer to that MFC object). However, there are a lot of parameters to a DC, and this means you end up, in a serious drawing routine, having a lot of odd variables to keep track of.
However, it is simpler than this. There is a serious underappreciated function, ::SaveDC, and its partner, ::RestoreDC, which are available as methods CDC::SaveDC and CDC::RestoreDC. Using these you can now change the code to avoid the use of any gratuitous variables:
void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(...); // parameters not shown int save = dc.SaveDC(); dc.SelectObject(&f); dc.TextOut(...); // whatever... dc.RestoreDC(save); } // destructors called here...
Actually, it doesn't matter how many changes you made in the DC; when you do the RestoreDC call, the DC is restored to whatever state it was just before the SaveDC. This means that all the objects selected into it after the SaveDC, all the changes in the text color, background color, styles, etc. are all erased.
There are interesting ways of using the integers to SaveDC in creative ways, but I'm not going to go into those, primarily because I don't do them myself. Instead, I'm going to show a class that makes all this very easy. There's no download for this code; it is so simple you can copy-and-paste it into your own file.
class CSaveDC { public: CSaveDC(CDC & dc) { sdc = &dc; saved = dc.SaveDC(); } CSaveDC(CDC * dc) { sdc = dc; saved = dc->SaveDC(); } virtual ~CSaveDC() { sdc->RestoreDC(saved); } protected: CDC * sdc; int saved; };
That's all there is to it! Note that it has two constructors, one if you have a CDC * and one if you have a CDC or CDC &. All you do is declare a dummy variable. But there's an important trick, illustrated below.
void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(...); // parameters not shown { /* save context */ CSaveDC sdc(dc); dc.SelectObject(&f); dc.TextOut(...); // whatever... } /* save context */ } // destructors called here...
Note that the save context you create by using the CSaveDC class must be in a scope that is smaller than the objects selected into the DC. Thus the /* save context */ block guarantees that the CSaveDC destructor is guaranteed to be called before the destructor for the font. All you have to do is declare your fonts, pens, brushes, and regions outside the /* save context */ block and you can be guaranteed that their destructors will be called in a context where they are not selected into the active DC.
Note that CSaveDCs can nest (because the ::SaveDC can nest). The nesting can be static or dynamic. For example
void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(12,...); // most parameters not shown { /* save context */ CSaveDC sdc(dc); dc.SelectObject(&f); drawboxes(dc); dc.TextOut(...); // whatever... } // destructors called here void CMyView::drawboxes(CDC & dc) { CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0)); CBrush GreenBrush(RGB(0, 255, 0); CFont f; f.CreateFont(6, ...); // most parameters not shown { /* save context */ dc.SelectObject(&RedPen); dc.SelectObject(&GreenBrush); dc.SetBkMode(TRANSPARENT); dc.SetTextColor(::GetSysColor(COLOR_GRAYTEXT); ... } /* save context */ }
Note that the save context in drawboxes contains lots of changes; these will be undone when you leave the save context, so that when drawboxes returns to OnPaint, the DC will have the correct font (12-pixel), background mode, etc.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.