Font Management in a CView- or CWnd-derived class |
|
One of the problems of handling views is the problem of SetFont and GetFont.
A view does not actually implement these operations. The superclass CWnd::SetFont and CWnd::GetFont simply cause a SendMessage(WM_SETFONT,...) and SendMessage(WM_GETFONT). But the base window class has no handlers for these messages, and therefore they do nothing. So if you derive a class from either CView or directly from CWnd you will not get correct behavior.
Typically, SetFont will give the illusion of working, although it does nothing. And GetFont will usually return a NULL pointer, because the WM_GETFONT message returned a NULL font handle.
If you wish to handle GetFont and SetFont in your derived class, then you must either derive from a class that supports fonts, or add your own font support.
For example, certain variants of CStatic have font management. Sadly, Microsoft has "optimized" the behavior of static controls by not bothering to generate a WM_PAINT message, but just calls the internal paint handler. This causes an annoying flash if the application sends a WM_SETTEXT and you think you can draw the text yourself in your OnPaint handler. It is very sad, because incorrect behavior is not an "optimization"; the correct term for such coding is "a bug". But not all static controls have WM_SETFONT/WM_GETFONT handler; for example, frames and rectangles (SS_xxxFRAME and SS_xxxRECT) styles. So, for example, if you are creating a control, it is always nice to put code like this in your PreSubclassWindow:
ASSERT( (GetStyle() & (SS_BLACKFRAME | SS_BLACKRECT | SS_GRAYFRAME | SS_GRAYRECT | SS_WHITEFRAME | SS_WHITERECT)) == 0); // These styles not supported
The handlers for CWnd::SetFont and CWnd::GetFont are defined in afxwin.h:
class CWnd : public CCmdTarget { ...lots of stuff... public: void SetFont( _In_ HFONT hFont, _In_ BOOL bRedraw = TRUE) throw() { ATLASSERT(::IsWindow(m_hWnd)); ::SendMessage(m_hWnd, WM_SETFONT, (WPARAM)hFont, MAKELPARAM(bRedraw, 0)); } HFONT GetFont() const throw() { ATLASSERT(::IsWindow(m_hWnd)); return (HFONT)::SendMessage(m_hWnd, WM_GETFONT, 0, 0); }
If you want to implement SetFont and GetFont for your view, you have to actually implement handlers and state.
First, you need to have something that holds the state. I have chosen to implement this as an HFONT. So modify your class definition by adding the state variable:
class CMyView : public CView { ... stuff protected: HFONT font; };
Now you need to add virtual methods to your own class manage this. Due to some really poor decisions done in the 16-bit version of MFC, functions that should have been virtual are, in fact, not virtual. But we have a second "virtual method" technique: message handlers. So we have to add handlers for the messages.
class CMyView : public CView { ...stuff protected: afx_msg LRESULT OnSetFont(WPARAM, LPARAM); afx_msg LRESULT OnGetFont(WPARAM, LPARAM);
Due to some strange and, as far as I'm concerned, erroneous decisions made in 32-bit MFC, apparently made to maintain compatibility with the erroneous Win16 implementation, message handlers are created as public methods. This could never, ever, under any scenario, make sense. These handlers must not be called by anyone outside the class, so they should be protected. Due to even more bizarre decisions, the mechanisms that add handlers to the header file put them in some "random" order, based on the order on which you add them, instead of something intelligent, like putting everything together in groups. So I end up hand-editing the header files so they make sense. It is most unfortunate that our tools now create some of the most slovenly examples of coding that could ever exist. Properly-built tools do not create code we would fail a student for handing in as a Freshman exercise.
Now, having added the variable and the handler declarations to the class, they have to be connected with the messages.
You have to hand-edit the MESSAGE_MAP to create the entries, because due to some oversight on the part of Microsoft, only some messages are in the message handler options. I fail to understand how this decision was determined to make sense.
BEGIN_MESSAGE_MAP(CMyView, CView) ...whatever else you had here ON_MESSAGE(WM_SETFONT, OnSetFont) ON_MESSAGE(WM_GETFONT, OnGetFont) END_MESSAGE_MAP()
Now all you have to do is implement the handlers.
The OnSetFont handler merely stores the handle in the HFONT variable. If the "redraw" parameter, as specified by the LPARAM, is nonzero, a redraw will be forced. This is done by invalidating the entire client area. Note that the redrawing will not actually take place until the message pump dispatches a WM_PAINT message, which it does if the message queue becomes empty and there are no pending timer messages, after you return to the message pump. Thus, you can change fonts many times without incurring actual redrawing costs. Or do other updates that have redraw options, without getting a lot of screen flicker.
Screen flicker is bad. One of the more childish approaches to "optimization" ends up ignoring the effects of screen flicker on the end user. Flicker in the range of 8-20Hz is fatiguing for anyone, and can trigger severe migraine headaches in those who are vulnerable (such as I am) and epileptic seizures in others (in one famous incident in Japan, a children's cartoon with very rapid flashing of bright colors sent hundreds of children to emergency rooms). I cannot emphasize strongly enough that any expenditure of code and time to reduce flicker is not only justified, but essential.
LRESULT CFontSpeedView::OnSetFont(WPARAM wParam, LPARAM lParam) { font = (HFONT)wParam; if((BOOL)lParam) Invalidate(); return 0; } // CFontSpeedView::OnSetFont
The GetFont handler is equally simple. But note that although the GetFont method is declared as const, the OnGetFont handler must not be declared as const. The reason is that the ON_MESSAGE handler will generate a compilation error if the prototype of the method is not exactly LRESULT name(WPARAM, LPARAM). The prototype LRESULT name(WPARAM, LPARAM) const will not compile.
LRESULT CFontSpeedView::OnGetFont(WPARAM, LPARAM) { return (LRESULT)font; } // CFontSpeedView::OnGetFont
This is all that is required to implement GetFont and SetFont.
However, if you start with the HFONT object being NULL, then CWnd::GetFont will return NULL, and code like this will fail horribly:
So, you say, "That's poor code. It is important to test the result of GetFont" and you fix it by modifying the code to say
CFont * f = GetFont(); if(f != NULL) { /* has font LOGFONT lf; f->GetLogFont(&lf); ... } /* has font */
but this means the desired code will simply never be executed, because the CFont * is NULL. It doesn't crash and burn horribly, but it doesn't work, either!
LOGFONT lf; GetFont()->GetLogFont(&lf);
The solution to this is to set a default font in the OnInitialUpdate handler. The simplest solution is to use CreateStockObject:
void CMyView::OnInitialUpdate() { CView::OnInitialUpdate(); CFont f; f.CreateStockObject(DEFAULT_GUI_FONT); SetFont(&f); }
Note that even when the DeleteObject is called in CFont::~CFont, the fact that it is a stock object means that the handle will not be invalidated!
Calling ::DeleteObject on a stock object is a well-defined action, and the action is to ignore the deletion request. In Win16, the behavior was ill-defined and could actually result in the deletion of a stock object, which was a very poor implementation. It is well-defined in Win32, and is defined to do nothing to a stock object.
Note that the CWnd::SetFont and CWnd::GetFont are defined in terms of a CFont *:
void SetFont(CFont* pFont, BOOL bRedraw = TRUE); CFont* GetFont() const;
To understand why we cannot write our handlers in terms of CFont *, we need to understand the concept of handle maps.
A handle-map is a thread-specific associative lookup which maps values which are handle values to objects which are MFC objects.
Handle type | MFC type |
HWND | CWnd * |
HFONT | CFont * |
HPEN | CPen * |
HBRUSH | CBrush * |
HPALETTE | CPalette * |
SOCKET | CAsyncSocket * |
HBITMAP | CBitmap * |
HRGN | CRgn * |
HMENU | CMenu * |
Note that we have implemented the font variable as an HFONT, not as a CFont *. Why did we do this? Well, because all we get in the message is an HFONT. We don't know if there actually is a CFont * that corresponds to the HFONT. This means that if we try to form a CFont * from an HFONT using the handle map, the CFont::FromHandle method might create a temporary CFont * in the handle map. A temporary object has an existence that is transient, and will be deleted by the default CWinApp::OnIdle handler. This would mean that if we used a CFont *, it could evaporate. But an HFONT is valid as long as the font exists, and even if CFont::DeleteObject is called at some point, the HFONT remains "valid" until presented to the kernel to act upon. If a handle which represents an object has been deleted, the kernel will balk and return a 0 or FALSE value for the API. But if the CFont * value is invalidated, your app can crash if memory is corrupted. You will likely crash with an access fault (0xC0000005).
Now that you've set the font, you are going to have to use it. For example, in your OnDraw handler, you will see
void CMyView::OnDraw(CDC * pDC) { ... CFont * f = GetFont(); pDC->SelectObject(f); ... }
However, note that in MFC, the OnPrepareDC method is called implicitly by the framework, and this function is virtual:
virtual void CView::OnPrepareDC(CDC * pDC, CPrintInfo * pInfo = NULL)
The MFC framework calls this method before calling OnDraw, so you can do any DC setup you want in this method. One of the common things you would do in the OnPrepareDC method is the above two lines of code to get the font and select it into the DC.
However, if you are doing a custom control, for example, on derived from CWnd, you would do your font management in the OnPaint handler:
void CMyCustomControl::OnPaint() { CPaintDC dc(this); CFont * f = GetFont(); dc.SelectObject(f); ...stuff }
If you want to implement the notions of SetFont and GetFont for your window, most commonly for your CView-derived class, you are on your own to do so. The font is not implicitly selected into a DC if you do GetDC, although any MFC program that does GetDC is probably poorly written. If you need a DC, you will use CClientDC to obtain a DC. You can be reasonably certain that if you write a GetDC, you have made an error.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.