Drawing Techniques |
|
Recently there have been a number of questions about drawing techniques. Almost all of these were using code far more complex than is required. This essay tells about how I do drawing in Windows. Being lazy, I wish to do it with as little effort as possible. I've developed some effective techniques over the years.
One of the most frequent misconceptions I've seen is that you have to allocate a drawing object in order to use it. So I see code of the form
CPen * myPen = new CPen; CPen->Create(...); CPen * OldPen = dc->SelectObject(myPen); ... delete myPen;
This is unnecessarily complex code. At least part of the confusion is that the SelectObject method wants a CPen * (or in general, a "Tool *"), which leads programmers to believe that they must supply a CPen * variable. Nothing in the specification of the call requires that the object be allocated on the heap; only that a pointer to an object be provided. This can be done by doing
CPen myPen(...); CPen * OldPen = dc->SelectObject(&myPen); ... dc->SelectObject(OldPen);
This is much simpler code; it doesn't call the allocator. And the parameter &myPen satisfies the requirement of a CPen *. Since pens and other GDI tools are often created "on the fly" and discarded afterwards, there is no need to allocate them on the heap. When the destructor is called when you leave scope, the HPEN underlying the CPen is normally destroyed--but see below for when it is not!
The only time you need to keep objects around is if you need to return them to a context outside your own. For example, the following will not work properly:
HBRUSH MyWnd::OnCtlColor(...) { CBrush MyBackground(RGB(255, 0, 0)); return (HBRUSH)MyBackground; }
This does not work because upon exiting the context in which the brush was created, the HBRUSH is destroyed, so the handle, when it is finally used by the caller, represents an invalid GDI object and is ignored by GDI. But the following is also erroneous:
HBRUSH MyWnd::OnCtlColor(...) { CBrush * MyBackground = new CBrush(RGB(255, 0, 0)); return (HBRUSH)*MyBackground; // or return (HBRUSH)MyBackground->m_hObject; }
This is erroneous because a brush is allocated each time the routine is called, and is never deleted. Not only do you clutter up your application space with a lot of unreclaimed CBrush objects, you clutter up the GDI space with a lot of unreclaimed HBRUSH objects. This will eventually crash Win9x, and on NT will eventually crash your application (it just takes longer on NT because you have more space to fill up).
In cases like this, you must add a member variable to your class, the background brush, e.g.,
CBrush * MyBackground;
initialize it in the constructor, and delete it in the destructor:
MyWnd::MyWnd() { ... MyBackground = new CBrush(RGB(255,0,0)); } MyWnd::~MyWnd() { ... delete MyBackground; }
The implicit deletion of objects in the destructor doesn't work if the object is selected into an active DC when it goes out of scope. The ::DeleteObject is called, but because the object is in a DC, this operation fails. Thus the following code leaks GDI objects, and eventually all of the GDI space will will up:
void OnDraw(CDC * pDC) { CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0)); ... pDC->SelectObject(&RedPen); ... }
At the time we leave scope, the HPEN associated with RedPen is still selected into the DC. The destructor is called, but is ignored. The HPEN is not deleted.
The correct solution to this is to be certain that none of your GDI objects are selected into the DC when the destructors are called. The usual way is to save the old object. This means you have to remember to save it, and remember to restore it, and you don't need to save any but the original, for example,
{ CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0)); CPen GreenPen(PS_SOLID, 0, RGB(0, 255, 0)); CPen BluePen(PS_SOLID, 0, RGB(0, 0, 255)): CPen * OldPen = dc->SelectObject(&RedPen); ... dc->SelectObject(&GreenPen); ... dc->SelectObject(&BluePen); ... dc->SelectObject(OldPen); }
Note that only the original pen needs to be restored. But what if there were a loop? You'd need to store the original pen the first time, or always restore it at the end of the loop so the next iteration was correct, etc. And what if you needed to change pen, brush, ROP, fill mode, etc., etc. Very tedious. And what if you decided to change pens earlier in the code? The hazards of compromising what I call "robustness under maintenance" are considerable. The simplest way to handle this is to use SaveDC/RestoreDC instead:
{ CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0)); CPen GreenPen(PS_SOLID, 0, RGB(0, 255, 0)); CPen BluePen(PS_SOLID, 0, RGB(0, 0, 255)): int saved = dc->SaveDC(); dc->SelectObject(&RedPen); ... dc->SelectObject(&GreenPen); ... dc->SelectObject(&BluePen); ... dc->RestoreDC(saved); }
Note that there is no reason to maintain a bunch of variables whose sole purpose is to restore the DC to what it was, and remember which ones, and how to manage them, and a lot of other needless complexity. Just do a RestoreDC. All of the DC state is restored to whatever it was when the SaveDC was done, which means that all GDI objects selected into the DC are deselected. Now when their destructors are called, the underlying GDI objects will be destroyed, because they are not in any active DC.
SaveDC and RestoreDC will "nest", in that you can call a function that does some drawing and it can do its own save/restore, or you can just do it inline in your function, e.g.,
{ CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0)); CPen GreenPen(PS_SOLID, 0, RGB(0, 255, 0)); CPen BluePen(PS_SOLID, 0, RGB(0, 0, 255)): int saved = dc->SaveDC(); dc->SelectObject(&RedPen); ... dc->SelectObject(&GreenPen); int saved2 = dc->SaveDC(); for(int i = 0; i < something; i++) { dc->SelectObject(...); ... dc->SelectObject(...); } dc->RestoreDC(saved2); ... dc->SelectObject(&BluePen); ... dc->RestoreDC(saved); }
The only requirement is that any GDI objects you create must be at the same or enclosing scope of the SaveDC and must not have their destructors called until after the RestoreDC. I usually solve this by requiring that the GDI objects and the variable for the save/restore be in the identical scope. For example, this will not work correctly:
int saved2 = dc->SaveDC(); for(int i = 0; i < something; i++) { CBrush br(RGB(i, i, i)); dc->SelectObject(&br); ... dc->SelectObject(&GreenPen); } dc->RestoreDC(saved2);
because when the destructor is called for br it is still selected into the DC. I can't move the creation of br outside the loop because it depends on the loop variable i. There are two solutions to this: the traditional one of saving the old brush and restoring it explicitly, or doing a SaveDC/RestoreDC inside the loop:
int saved2 = dc->SaveDC(); for(int i = 0; i < something; i++) { CBrush br(RGB(i, i, i)); CBrush * oldBrush = dc->SelectObject(&br); ... dc->SelectObject(&GreenPen); dc->SelectObject(oldBrush); } dc->RestoreDC(saved2);
int saved2 = dc->SaveDC(); for(int i = 0; i < something; i++) { int save = dc->SaveDC(); CBrush br(RGB(i, i, i)); dc->SelectObject(&br); ... dc->SelectObject(&GreenPen); dc->RestoreDC(save); } dc->RestoreDC(saved2);
I have not done any performance measurement, so I don't know if SaveDC in a tight loop really matters; evidence suggests that for most drawing routines on most fast computers, this overhead is unnoticeable. but it could show up more seriously in a high-performance inner loop drawing routine. When the save of the old value and restore of the new value are separated by only a few lines, in an inner loop, I will revert to the older mechanism, but if the extent of the code goes beyond more than about six lines I'll use SaveDC/RestoreDC because it is easier to not get this wrong.
There are some interesting pieces of code in the GUI for my Hook DLL; it draws a cute picture of a cat. You may find some interesting ideas reading this code as well.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.