Optimization and fonts
Optimization and Correctness
Back in the days of Win16, GDI resources were scarce and precious. They were
carefully hoarded. Programmers went to a lot of extremes to avoid running out
of GDI resources. This meant that if you ever created a font, you created it
once and used it as often as possible, and deleted it when your program terminated.
This was a great optimization, and it was necessary then. It is not really
a good idea in modern Win32 systems. The reason is that it violates abstraction.
If I create a control that expects a special font, I should create that font
for that control, and not require the programmer create it for me. The
CFont object is therefore in the control subclass, not a global variable
or a variable of the CDialog class or CWinApp class. If all
instances of the control share the same font requirement, you can simplify things
a bit by using a static class member to hold the CFont, although you will
probably have to reference-count it to know when to delete it.
An optimization which renders a program incorrect, or potentially incorrect,
is not an optimization. Beware! Because the consequence of some optimizations
to "save space" actually can leak space. This Is Not Good
Changing Fonts in a Control
Changing a font in a control requires that you delete the existing font. This
is why it is a Good Idea to have a font used in only one control, and not shared.
The following code is correct, and does not leak fonts everywhere:
void CMyControl::ChangeFont()
{
CFont f;
f.CreateFont(...);
CFont * oldfont = GetFont();
if(oldfont != NULL)
oldfont->DeleteObject(); // Be Careful! See discussion on below!
SetFont(&f);
f.Detach();
}
This has the advantage that it doesn't leak HFONTs each time the font
is changed. It is left as an Exercise For The Reader to figure out how the parameters
to CreateFont are specified; I find that I usually provide as parameters
to my ChangeFont method the size, the face name, and a bold flag, and
that encompasses 99% of what I do in changing fonts in controls. In generalizing
the above example, you have to figure out what your needs are.
The GetFont returns a reference to a CFont object, unless there
was no font associated with the control, in which case the result is NULL.
But if the result is non-NULL, I delete the HFONT by calling DeleteObject.
If the font was one of mine, it is now deleted (and if it was shared, the other
users of the font are in trouble--but we'll get to that shortly). If the font
was a stock font, the DeleteObject has no effect. The SetFont then
sets the font, and the Detach, as I just explained, keeps the HFONT
from following the CFont into the Great Bit Bucket In The Sky.
Whoa! What about that CFont * we got? Aren't we leaking something there?
No, because the CFont * that was created by GetFont is a temporary
MFC Object, which means that it is added to a list of garbage-collectable
objects. The next time there is some idle time, the default OnIdle method
goes around and cleans up all the temporary objects by deleting them. In fact,
if you say "delete oldfont" you will eventually get an ASSERT
failure from the temporary-object collector, which tells you that you've Done
Something Bad to its data.
A caveat of the above code
There's a bug in the above code. It is not an apparent bug, but a reader of
this essay found it and pointed it out to me. I need to be more explicit here.
You should do DeleteObject only to fonts you have created. The situation
was this: the reader did exactly what I showed, and complained that although
his buttons how had the desired font, all the other buttons now had the wrong
font. Whoops. This was my error. What happens in a dialog is that a font is created
for the controls in the dialog, and a SetFont is done for each control
as it is created.
What had happened was that by following my advice, he managed to delete
the dialog font, so all the other controls fell into the default case. The proper
specification is that you should delete the old font only for fonts you have
created, and not for the default font of a control. How can you tell the difference?
Well, if you are in the OnInitDialog handler, you know you didn't create
the font, so you shouldn't delete the existing font. I have not checked this
out, but I believe the dialog will delete its copy of this font when it is closed.
What if you are changing the font later, and don't know if you created
the font the last time or not? Well, the obvious way is to keep some sort of
Boolean around saying whether or not you changed the font. A typical case might
be when you are using a CListBox as a logging control, and want the user
to be able to change the font. This is needlessly complex. When I've had to do
this (note that I already knew better when I wrote the example! What
I did was do a GetFont for the existing control. Then I created a new
font which was identical to it and set that font in the control. The code
to do this is shown below:
// Create a new font so we can change it later
CFont * f = c_MyLogListBox.GetFont();
CFont newfont;
LOGFONT lf;
if(f != NULL)
{ /* Set up duplicate font */
f->GetObject(sizeof(LOGFONT), &lf);
newfont.CreateFontIndirect(&lf);
} /* Set up duplicate font */
else
{ /* Use default font spec */
newfont.CreateStockObject(ANSI_VAR_FONT);
} /* Use default font spec */
c_MyLogListBox.SetFont(newfont);
newfont.Detach();
After you do this, you can safely use my previous example to delete the previous
font because you know it is one that you created yourself.
Thanks, and a wave of the Flounder Fin to Michael Mueller for taking the time
to point out this problem.