Introduction
We will adapt the view of an MFC application so that a user can select a font from a standard Windows dialog box, which will automatically update the contents of the view with the new font.
MFC Framework: Determining where Font Selection should be handled
First, we have to define a command handler for the ID_SELECTFONT
Options menu item. But what part of the MFC framework should be responsible for handling this command? As a rule of thumb, you should consider what to do for each application class, and choose the method that has the least work to do.
Here are four choices, sorted from worst to best:
- Application: it seems hard to be able to "touch" each view,
AfxGetMainWnd()
provides a pointer to the frame;
- Frame window:
GetActiveView()
allows you to get a pointer to the active view only;
- View: each view must duplicate the same code and, in addition, notify the other views;
- Document: easy to synchronize each view with
UpdateAllViews()
.
As you can see, the Document is the best candidate for this task. Once the command is received, the document will notify each attached view through the same mechanism, that is UpdateAllViews()
. So, add a OnSelectFont()
command handler to C???Doc
.
How to set Windows Fonts
Use WM_SETFONT and HFONT
As you are well aware, a font is a window attribute. When you want a window to change its display to a new font, you send a WM_SETFONT
message to it with an HFONT
as parameter. This works for every Windows common control, such as an edit field, a list view or a tree view.
Now that you know how to change a window font, we have to learn how to get a font, or to be more precise, an HFONT
. MFC provides a CFont
class which wraps an HFONT
, but we will manipulate HFONT
directly, since this will be easier for us. When you want a font, you need to declare it logically through a logical font, described in Windows as a LOGFONT
structure:
typedef struct tagLOGFONT
{
LONG lfHeight;
LONG lfWidth;
LONG lfEscapement;
LONG lfOrientation;
LONG lfWeight;
BYTE lfItalic;
BYTE lfUnderline;
BYTE lfStrikeOut;
BYTE lfCharSet;
BYTE lfOutPrecision;
BYTE lfClipPrecision;
BYTE lfQuality;
BYTE lfPitchAndFamily;
TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT;
Don't fret, you will not have to guess which value to set in each LOGFONT
member, Windows provides several Win32 API functions to help you. Once a font is logically defined, you ask Windows to give you back an HFONT
handle to a physical font which can be really used through the CreateFontIndirect()
Win32 function.
Standard Windows dialog for common font selection
Now we know how to transform a logical font into a real font that can be sent to a window, the question is how do we get the logical font? Among the set of common dialog boxes provided by Windows, you will find a standard "font picker" (or Font) dialog. If you call the Win32 API function ChooseFont()
, you get a logical font filled with the description of the font which is selected by the user from the standard Font dialog:
Here is its declaration:
BOOL ChooseFont(LPCHOOSEFONT lpcf);
If a font has been selected, it returns TRUE
and FALSE
otherwise. You have to define a CHOOSEFONT
structure before calling it:
typedef struct
{
DWORD lStructSize;
HWND hwndOwner;
HDC hDC;
LPLOGFONT lpLogFont;
INT iPointSize;
DWORD Flags;
DWORD rgbColors;
LPARAM lCustData;
LPCFHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
HINSTANCE hInstance;
LPTSTR lpszStyle;
WORD nFontType;
WORD ___MISSING_ALIGNMENT__;
INT nSizeMin;
INT nSizeMax;
} CHOOSEFONT;
We are just interested in the lpLogFont
member which points to a LOGFONT
structure used to set the currently selected font when the dialog box is opened. (Flags must be set to the value CF_INITTOLOGFONTSTRUCT
). Once ChooseFont()
returns TRUE
, lpLogFont
contains the font�s logical description.
If you want to dig deeper into font manipulation, you should refer to the following Visual C++ samples: TTfont
s, FontView
and GridFont
.
Code Reusability: Writing a Font Helper Class
We will create a helper class to wrap font manipulation. First, we need to list each feature needed. Next, we will try to find out which parameter is required to initialize it. Finally, we will write a class on its own, to be integrated with C???Doc
.
Definition of CFontHelper
This class will wrap a font in order to help us handle logical and physical fonts. Here is what we would expect from such a class:
- Give access to its logical description and its corresponding font handle
- Save and load itself
- Display the Windows common font picker dialog and store the corresponding font
- Create a default font
To implement these features, some parameters are required. A section and several entry strings are needed to save and restore its font description. Also, we should allow different ways to create such an object:
- From scratch, and in this case a default font is created.
- From a description already saved under a section and entry.
Class creation
Now we have to turn the features listed above into code. Create a new class with the following declaration:
class CFontHelper : public CObject
{
public:
CFontHelper();
CFontHelper(LPCTSTR szSection, LPCTSTR szEntry);
~CFontHelper();
public:
HFONT CreateFont();
LOGFONT GetLogFont(LOGFONT& LogFont);
HFONT GetFontHandle();
void Save();
void Restore();
void SetSectionName(LPCTSTR szSection);
void SetEntryName(LPCTSTR szEntry);
BOOL ChooseFont();
protected:
void DefaultFontInit();
void DefaultInit();
protected:
HFONT m_hFont;
LOGFONT m_LogFont;
CString m_szSection;
CString m_szEntry;
};
We store both logical and physical definitions of a font with m_LogFont
and m_hFont
members. One of the tasks of the class is to keep both parameters consistent within its methods.
Implementation of CFontHelper
The CFontHelper
implementation is divided into four different parts: initialization, font picker dialog, save/restore definition, and logical/physical font management. Our class defines two constructors. The first is shown in this code sample:
CFontHelper::CFontHelper(LPCTSTR szSection, LPCTSTR szEntry)
{
ASSERT((szSection != NULL) && (_tcslen(szSection) > 0));
ASSERT((szEntry != NULL) && (_tcslen(szEntry) > 0));
m_szSection = szSection;
m_szEntry = szEntry;
DefaultFontInit();
Restore();
}
This constructor loads the font from a saved description under szSection
and szEntry
. It relies on both DefaultFontInit()
and Restore()
methods to retrieve the font description. Since save/restore parameters are provided, we only need to create a default font:
void CFontHelper::DefaultFontInit()
{
m_LogFont.lfHeight = -11;
m_LogFont.lfWidth = 0;
m_LogFont.lfEscapement = 0;
m_LogFont.lfOrientation = 0;
m_LogFont.lfWeight = FW_NORMAL;
m_LogFont.lfItalic = 0;
m_LogFont.lfUnderline = 0;
m_LogFont.lfStrikeOut = 0;
m_LogFont.lfCharSet = 0;
m_LogFont.lfOutPrecision = OUT_STRING_PRECIS;
m_LogFont.lfClipPrecision = CLIP_STROKE_PRECIS;
m_LogFont.lfQuality = DEFAULT_QUALITY;
m_LogFont.lfPitchAndFamily = FF_SWISS | VARIABLE_PITCH;
_tcscpy(m_LogFont.lfFaceName, _T("Lucida Sans Unicode"));
CreateFont();
}
You might wonder why we are creating a default font with CreateFont()
, since Restore()
will create a new one just a few milliseconds later. The reason is simple � error handling. If the given szSection
/szEntry
does not provide access to a valid font description, the CFontHelper
object would nevertheless be valid: it will contain a font, not the one expected but a default font.
The second constructor relies on DefaultInit()
to initialize its members with default values:
CFontHelper::CFontHelper()
{
DefaultInit();
}
And here is its helper function�s implementation:
void CFontHelper::DefaultInit()
{
DefaultFontInit();
m_szSection = _T("Settings");
m_szEntry = _T("Font");
}
It creates the default font and sets save/restore keys with default values.
Windows common Font dialog picker
Once a CFontHelper
is initialized, you can ask it to display the Windows Font picker dialog:
BOOL CFontHelper::ChooseFont()
{
CHOOSEFONT choosefont;
LOGFONT LogFont = m_LogFont;
choosefont.lStructSize = sizeof(CHOOSEFONT);
choosefont.hwndOwner = ::GetActiveWindow();
choosefont.hDC = 0;
choosefont.lpLogFont = &LogFont;
choosefont.iPointSize = 0;
choosefont.Flags = CF_SCREENFONTS|CF_INITTOLOGFONTSTRUCT;
choosefont.rgbColors = 0;
choosefont.lCustData = 0;
choosefont.lpfnHook = 0;
choosefont.lpTemplateName = NULL;
choosefont.hInstance = 0;
choosefont.lpszStyle = 0;
choosefont.nFontType = SCREEN_FONTTYPE;
choosefont.nSizeMin = 0;
choosefont.nSizeMax = 0;
if (::ChooseFont(&choosefont) != FALSE)
{
m_LogFont = LogFont;
CreateFont();
return TRUE;
}
else
return FALSE;
}
This method prepares a call to the Win32 API ChooseFont()
function. The CHOOSEFONT
structure is therefore initialized to set the behavior we want:
- The Font dialog will be modal for the current active window (main frame in our case).
- The currently selected font will be the one described by
LogFont
.
- The logical font description is copied back into a temporary variable
LogFont
.
- Only screen fonts are presented by the dialog.
- If a font is selected, its logical description is saved in
m_LogFont
and CreateFont()
is called to create and save the corresponding font handle.
Management and lifetime of font description
The helper method used to transform a logical font into a real Windows font is called CreateFont()
:
HFONT CFontHelper::CreateFont()
{
HFONT hFont = ::CreateFontIndirect(&m_LogFont);
if (hFont == NULL)
{
TRACE("Impossible to create font\n");
}
if (m_hFont != NULL)
::DeleteObject(m_hFont);
m_hFont = hFont;
return hFont;
}
The implementation is straightforward and relies on the Win32 API CreateFontIndirect()
function. Once a logical font has been converted into a font handle, it is saved in m_hFont
. A font created by CreateFontIndirect()
must be freed in order to avoid a resource leak. Therefore, the previously stored font handle is deleted using DeleteObject()
before saving the new one.
Once CFontHelper
contains a valid font, you will want to use it. Two methods allow you to retrieve either a logical font or a font handle. Their implementation is obvious. They both return the member's value.
LOGFONT CFontHelper::GetLogFont(LOGFONT& LogFont)
{
LogFont = m_LogFont;
}
HFONT CFontHelper::GetFontHandle()
{
return m_hFont;
}
When it is time to destruct, CFontHelper
does not forget to release its resources:
CFontHelper::~CFontHelper()
{
if (m_hFont != NULL)
::DeleteObject(m_hFont);
}
Therefore, you should take care of deleting CFontHelper
only when you are sure the font you could have retrieved from it (with GetFontHandle()
, for example) is no longer in use.
Saving and restoring CFontHelper descriptions
A CFontHelper
object is able to save itself into an INI file or into the Registry. Four methods are responsible for doing this special serialization.
First, the object must know where its description will be saved and two methods offer the means to set these backup parameters:
void CFontHelper::SetSectionName(LPCTSTR szSection)
{
ASSERT((szSection != NULL) && (_tcslen(szSection) > 0));
m_szSection = szSection;
}
void CFontHelper::SetEntryName(LPCTSTR szEntry)
{
ASSERT((szEntry != NULL) && (_tcslen(szEntry) > 0));
m_szEntry = szEntry;
}
They both store section and entry values into their associated protected members m_szSection
and m_szEntry
.
Since a handle value has no meaning once saved, it is better to copy the font�s logical description.
void CFontHelper::Save()
{
ASSERT(m_szSection.GetLength() > 0);
ASSERT(m_szEntry.GetLength() > 0);
CString strBuffer;
strBuffer.Format("%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
m_LogFont.lfHeight,
m_LogFont.lfWidth,
m_LogFont.lfEscapement,
m_LogFont.lfOrientation,
m_LogFont.lfWeight,
m_LogFont.lfItalic,
m_LogFont.lfUnderline,
m_LogFont.lfStrikeOut,
m_LogFont.lfCharSet,
m_LogFont.lfOutPrecision,
m_LogFont.lfClipPrecision,
m_LogFont.lfQuality,
m_LogFont.lfPitchAndFamily);
AfxGetApp()->WriteProfileString (m_szSection,
m_szEntry + _T("_Desc"), strBuffer);
AfxGetApp()->WriteProfileString (m_szSection,
m_szEntry + _T("_Name"), m_LogFont.lfFaceName);
}
Each m_LogFont
numerical member is concatenated into a long string which is saved under a different entry than the font name. For example, here is the INI file layout where a font is stored by default:
[Setting]
Font_Desc=-11:0:0:0:400:0:0:0:0:3:2:1:66
Font _Name=Comic Sans MS
Here is the implementation of what happens when a font needs to be reloaded:
void CFontHelper::Restore()
{
ASSERT(m_szSection.GetLength() > 0);
ASSERT(m_szEntry.GetLength() > 0);
CString strBuffer=AfxGetApp()->GetProfileString(m_szSection,
m_szEntry + _T("_Desc"));
if (strBuffer.IsEmpty())
return;
LOGFONT LogFont;
int cRead = _stscanf (strBuffer,
"%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
&LogFont.lfHeight,
&LogFont.lfWidth,
&LogFont.lfEscapement,
&LogFont.lfOrientation,
&LogFont.lfWeight,
&LogFont.lfItalic,
&LogFont.lfUnderline,
&LogFont.lfStrikeOut,
&LogFont.lfCharSet,
&LogFont.lfOutPrecision,
&LogFont.lfClipPrecision,
&LogFont.lfQuality,
&LogFont.lfPitchAndFamily);
if (cRead != 13)
{
TRACE("Restore(): Invalid Registry/INI file entry\n");
return;
}
strBuffer = AfxGetApp()->GetProfileString(m_szSection,
m_szEntry + _T("_Name"));
if (strBuffer.GetLength() <= 0)
return;
_tcscpy(LogFont.lfFaceName, strBuffer);
m_LogFont = LogFont;
CreateFont();
}
The logical font description is retrieved in two steps: the numerical values first and then font name. Finally, a real font is created using CreateFont()
which sets m_hFont
to this new font handle.
How to use CFontHelper
Even if the helper class has been defined and explained, it is always easier to understand it using an example. Let�s see how to integrate CFontHelper
into our MFC application.
First, include the following header file to ???Doc.h:
#include "FontHelper.h"
Creation and saving in C???Doc
A CFontHelper
object is stored by C???Doc
as a protected member:
protected:
CFontHelper* m_pFontHelper;
It is created in the document�s constructor:
C???Doc::C???Doc()
{
...
m_pFontHelper = new CFontHelper(_T("Settings"), _T("Font"));
}
It is deleted in the document�s destructor as follows:
C???Doc::~C???Doc()
{
if (m_pFontHelper != NULL)
{
m_pFontHelper->Save();
delete m_pFontHelper;
}
}
As usual, we don't forget to save it before its deletion.
The role of the frame
It is the role of the frame to ask C???Doc
to set the font for its views, before it creates them. We know that OnCreateClient()
(which you may recall is a CMainFrame
method) is called to embed all views in the document. So, use Class Wizard to add this function to the main frame class. Copy the following code just before OnCreateClient()
returns:
if (pContext != NULL)
{
C???Doc* pDocument = (C???Doc*)pContext->m_pCurrentDoc;
pDocument->NotifyDefaultFont();
}
else
TRACE("Impossible to get frame creation parameters");
return TRUE;
}
In ???Doc.h, declare this method as public
:
public:
void NotifyDefaultFont();
Add the following source code to ???Doc.cpp:
void C???Doc::NotifyDefaultFont()
{
ASSERT(m_pFontHelper != NULL);
if (m_pFontHelper != NULL)
{
SetCurrentFont();
}
}
This is a wrapper around a synchronizing method called SetCurrentFont()
which will be introduced later.
Selecting a new font: OnSelectFont()
You may recall that the objective of this exercise was to allow the user to select a font, and we created a handler for the WM_SELFONT
message at the beginning of this section. We can now implement this function OnSelectFont()
:
void C???Doc::OnSelectFont()
{
ASSERT(m_pFontHelper != NULL);
if (m_pFontHelper != NULL)
{
if (m_pFontHelper->ChooseFont())
SetCurrentFont();
}
}
Our document asks m_pFontHelper
to present a Windows common font dialog picker to the user. Once a font has been selected, it needs to be sent to every associated view.
Synchronizing a font among views: UpdateAllViews() and OnUpdate()
Our document knows that its views must use a new font, but how do we tell them? Once again, we will use UpdateAllViews()
with a new custom lHint
.
Therefore, SetCurrentFont()
relies on UpdateAllViews()
to dispatch the new font to all attached views. Add a public method SetCurrentFont( )
to the document class and then add its implementation as follows:
void C???Doc::SetCurrentFont()
{
ASSERT(m_pFontHelper != NULL);
if (m_pFontHelper != NULL)
UpdateAllViews(NULL,2,
(CObject*)m_pFontHelper->GetFontHandle());
}
The parameter received by each view in their OnUpdate()
method will be the font handle provided by our font helper. You should now use Class Wizard to add the OnUpdate( )
method to your view class. Now, you have to add a new view message to OnUpdate()
for each view:
switch(lHint)
{
case 2:
{
SendMessage(WM_SETFONT, (WPARAM)(HFONT)pHint, 0L);
Invalidate(TRUE);
}
break;
}
The view sends a WM_SETFONT
message to itself and sets the input font handle as wParam
.
Additional Notes
For more information, please refer to Ivor Horton's Beginning Visual C++ by Wrox Press.