Introduction
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.
Tool or Tool *?
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!
When Allocation is Necessary
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;
}
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;
}
When Deletion Won't Work
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 fill 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.
Please leave your comments below with questions or comments about this article.
Copyright © 1999 The Joseph M. Newcomer Co. All Rights Reserved.
www.flounder.com/mvp_tips.htm
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.