Programming Windows with MFC, Second Edition
If you write an application whose primary user interface is a dialog-like collection of controls, you should consider using a dialog box as a main window. Charles Petzold immortalizes this technique with the HEXCALC program featured in his book Programming Windows. Scores of developers have used similar techniques for creating small, utility-type application programs whose main windows are more easily defined in dialog templates than within the programmatic confines of OnCreate handlers.
Writing a dialog-based application is a snap thanks to AppWizard. One of the options in AppWizard's Step 1 dialog box is a radio button labeled Dialog Based. Checking this button prompts AppWizard to generate an application whose main window is a dialog box. AppWizard creates the dialog resource for you and derives a dialog class from CDialog. It also emits a special version of InitInstance that instantiates the dialog class and calls its DoModal function to display the dialog box on the screen when the application is started. All you have to do is add controls to the dialog in the resource editor and write message handlers to respond to control events. The AppWizard-generated code handles everything else.
The DlgCalc application shown in Figure 8-9 is an example of a dialog-based MFC application. DlgCalc is a calculator applet. It differs from the calculator applet supplied with Windows in one important respect: it uses postfix notation, which is also known as reverse Polish notation, or RPN. Postfix notation is the form of data entry used by Hewlett-Packard calculators. Once you've grown accustomed to postfix notation, you'll never want to use a conventional calculator again.
Figure 8-9. The DlgCalc window.
DlgCalc's source code appears in Figure 8-10. The main window is created in CDlgCalcApp::InitInstance, which constructs a CDlgCalcDlg object, copies the object's address to the application object's m_pMainWnd data member, and calls DoModal to display the window:
CDlgCalcDlg dlg; m_pMainWnd = &dlg; dlg.DoModal (); |
CDlgCalcDlg is the dialog class that AppWizard derived from CDialog. The window created from it is a dialog box in every sense of the term, but it doubles as a main window since it has no parent and its address is tucked away in m_pMainWnd. I deleted some of the code that AppWizard placed in InitInstance—notably, the code that tests DoModal's return value—because it served no purpose in this application. I also deleted the WM_QUERYDRAGICON handler that AppWizard included in the dialog class and the AppWizard-generated OnPaint code that paints the application icon when the window is minimized because neither is needed unless your application will be run on old versions of Windows—specifically, versions that use the Windows 3.x_style shell.
Figure 8-10. The DlgCalc application.
DlgCalc.h
// DlgCalc.h : main header file for the DLGCALC application // #if !defined(AFX_DLGCALC_H__F42970C4_9047_11D2_8E53_006008A82731__INCLUDED_) #define AFX_DLGCALC_H__F42970C4_9047_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #ifndef __AFXWIN_H__ #error include `stdafx.h' before including this file for PCH #endif #include "resource.h" // main symbols /////////////////////////////////////////////////////////////////////////// // CDlgCalcApp: // See DlgCalc.cpp for the implementation of this class // class CDlgCalcApp : public CWinApp { public: CDlgCalcApp(); // Overrides // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation //AFX_MSG DECLARE_MESSAGE_MAP() }; /////////////////////////////////////////////////////////////////////////// // // Microsoft Visual C++ will insert additional declarations immediately // before the previous line. #endif // !defined(AFX_DLGCALC_H__F42970C4_9047_11D2_8E53_006008A82731__INCLUDED_) |
DlgCalc.cpp
// DlgCalc.cpp : Defines the class behaviors for the application. // #include "stdafx.h" #include "DlgCalc.h" #include "DlgCalcDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CDlgCalcApp BEGIN_MESSAGE_MAP(CDlgCalcApp, CWinApp) //AFX_MSG ON_COMMAND(ID_HELP, CWinApp::OnHelp) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CDlgCalcApp construction CDlgCalcApp::CDlgCalcApp() { } /////////////////////////////////////////////////////////////////////////// // The one and only CDlgCalcApp object CDlgCalcApp theApp; /////////////////////////////////////////////////////////////////////////// // CDlgCalcApp initialization BOOL CDlgCalcApp::InitInstance() { CDlgCalcDlg dlg; m_pMainWnd = &dlg; dlg.DoModal (); return FALSE; } |
DlgCalcDlg.h
// DlgCalcDlg.h : header file // #if !defined(AFX_DLGCALCDLG_H__F42970C6_9047_11D2_8E53_006008A82731__INCLUDED_) #define AFX_DLGCALCDLG_H__F42970C6_9047_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 /////////////////////////////////////////////////////////////////////////// // CDlgCalcDlg dialog class CDlgCalcDlg : public CDialog { // Construction public: void UpdateDisplay (LPCTSTR pszDisplay); CDlgCalcDlg(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgCalcDlg) enum { IDD = IDD_DLGCALC_DIALOG }; // NOTE: the ClassWizard will add data members here //}}AFX_DATA // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation protected: void DropStack(); void LiftStack(); void DisplayXRegister(); double m_dblStack[4]; double m_dblMemory; CString m_strDisplay; CString m_strFormat; CRect m_rect; int m_cxChar; int m_cyChar; BOOL m_bFixPending; BOOL m_bErrorFlag; BOOL m_bDecimalInString; BOOL m_bStackLiftEnabled; BOOL m_bNewX; HICON m_hIcon; HACCEL m_hAccel; // Generated message map functions //AFX_MSG afx_msg void OnDigit(UINT nID); DECLARE_MESSAGE_MAP() }; // // Microsoft Visual C++ will insert additional declarations immediately // before the previous line. #endif // !defined( // AFX_DLGCALCDLG_H__F42970C6_9047_11D2_8E53_006008A82731__INCLUDED_) |
DlgCalcDlg.cpp
// DlgCalcDlg.cpp : implementation file // #include "stdafx.h" #include "DlgCalc.h" #include "DlgCalcDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CDlgCalcDlg dialog CDlgCalcDlg::CDlgCalcDlg(CWnd* pParent /*=NULL*/) : CDialog(CDlgCalcDlg::IDD, pParent) { //AFX_DATA_INIT m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); m_hAccel = ::LoadAccelerators (AfxGetInstanceHandle (), MAKEINTRESOURCE (IDR_ACCEL)); m_bFixPending = FALSE; m_bErrorFlag = FALSE; m_bDecimalInString = FALSE; m_bStackLiftEnabled = FALSE; m_bNewX = TRUE; for (int i=0; i<4; i++) m_dblStack[i] = 0.0; m_dblMemory = 0.0; m_strFormat = _T ("%0.2f"); } void CDlgCalcDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgCalcDlg, CDialog) //AFX_MSG_MAP ON_CONTROL_RANGE (BN_CLICKED, IDC_0, IDC_9, OnDigit) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CDlgCalcDlg message handlers BOOL CDlgCalcDlg::OnInitDialog() { CDialog::OnInitDialog(); // // Set the application's icon. // SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE); // // Remove the Size and Maximize commands from the system menu. // CMenu* pMenu = GetSystemMenu (FALSE); pMenu->DeleteMenu (SC_SIZE, MF_BYCOMMAND); pMenu->DeleteMenu (SC_MAXIMIZE, MF_BYCOMMAND); // // Initialize m_rect with the coordinates of the control representing // the calculator's output window. Then destroy the control. // CWnd* pWnd = GetDlgItem (IDC_DISPLAYRECT); pWnd->GetWindowRect (&m_rect); pWnd->DestroyWindow (); ScreenToClient (&m_rect); // // Initialize m_cxChar and m_cyChar with the average character width // and height. // TEXTMETRIC tm; CClientDC dc (this); dc.GetTextMetrics (&tm); m_cxChar = tm.tmAveCharWidth; m_cyChar = tm.tmHeight - tm.tmDescent; // // Initialize the calculator's output window and return. // DisplayXRegister (); return TRUE; } void CDlgCalcDlg::OnPaint() { CPaintDC dc (this); dc.DrawEdge (m_rect, EDGE_SUNKEN, BF_RECT); UpdateDisplay (m_strDisplay); } BOOL CDlgCalcDlg::PreTranslateMessage(MSG* pMsg) { if (m_hAccel != NULL) if (::TranslateAccelerator (m_hWnd, m_hAccel, pMsg)) return TRUE; return CDialog::PreTranslateMessage (pMsg); } BOOL CDlgCalcDlg::OnCommand(WPARAM wParam, LPARAM lParam) { int nID = (int) LOWORD (wParam); if (m_bErrorFlag && (nID != IDC_CLX)) { ::MessageBeep (MB_ICONASTERISK); return TRUE; } if (m_bFixPending && ((nID < IDC_0) ¦¦ (nID > IDC_9)) && (nID != IDC_CLX)) { ::MessageBeep (MB_ICONASTERISK); return TRUE; } return CDialog::OnCommand (wParam, lParam); } void CDlgCalcDlg::OnDigit(UINT nID) { TCHAR cDigit = (char) nID; if (m_bFixPending) { m_strFormat.SetAt (3, cDigit - IDC_0 + 0x30); DisplayXRegister (); m_bFixPending = FALSE; m_bStackLiftEnabled = TRUE; m_bNewX = TRUE; return; } if (m_bNewX) { m_bNewX = FALSE; if (m_bStackLiftEnabled) { m_bStackLiftEnabled = FALSE; LiftStack (); } m_bDecimalInString = FALSE; m_strDisplay.Empty (); } int nLength = m_strDisplay.GetLength (); if ((nLength == MAXCHARS) ¦¦ ((nLength == (MAXCHARS - 10)) && !m_bDecimalInString)) ::MessageBeep (MB_ICONASTERISK); else { m_strDisplay += (cDigit - IDC_0 + 0x30); UpdateDisplay (m_strDisplay); m_dblStack[0] = _tcstod (m_strDisplay.GetBuffer (0), NULL); } } void CDlgCalcDlg::OnAdd() { m_dblStack[0] += m_dblStack[1]; DisplayXRegister (); DropStack (); m_bStackLiftEnabled = TRUE; m_bNewX = TRUE; } void CDlgCalcDlg::OnSubtract() { m_dblStack[0] = m_dblStack[1] - m_dblStack[0]; DisplayXRegister (); DropStack (); m_bStackLiftEnabled = TRUE; m_bNewX = TRUE; } void CDlgCalcDlg::OnMultiply() { m_dblStack[0] *= m_dblStack[1]; DisplayXRegister (); DropStack (); m_bStackLiftEnabled = TRUE; m_bNewX = TRUE; } void CDlgCalcDlg::OnDivide() { if (m_dblStack[0] == 0.0) { m_bErrorFlag = TRUE; ::MessageBeep (MB_ICONASTERISK); UpdateDisplay (CString (_T ("Divide by zero"))); } else { m_dblStack[0] = m_dblStack[1] / m_dblStack[0]; DisplayXRegister (); DropStack (); m_bStackLiftEnabled = TRUE; m_bNewX = TRUE; } } void CDlgCalcDlg::OnEnter() { LiftStack (); DisplayXRegister (); m_bStackLiftEnabled = FALSE; m_bNewX = TRUE; } void CDlgCalcDlg::OnChangeSign() { if (m_dblStack[0] != 0.0) { m_dblStack[0] = -m_dblStack[0]; if (m_strDisplay[0] == _T (`-')) { int nLength = m_strDisplay.GetLength (); m_strDisplay = m_strDisplay.Right (nLength - 1); } else m_strDisplay = _T ("-") + m_strDisplay; UpdateDisplay (m_strDisplay); } } void CDlgCalcDlg::OnExponent() { if (((m_dblStack[1] == 0.0) && (m_dblStack[0] < 0.0)) ¦¦ ((m_dblStack[1] == 0.0) && (m_dblStack[0] == 0.0)) ¦¦ ((m_dblStack[1] < 0.0) && (floor (m_dblStack[0]) != m_dblStack[0]))) { m_bErrorFlag = TRUE; ::MessageBeep (MB_ICONASTERISK); UpdateDisplay (CString (_T ("Invalid operation"))); } else { m_dblStack[0] = pow (m_dblStack[1], m_dblStack[0]); DisplayXRegister (); DropStack (); m_bStackLiftEnabled = TRUE; m_bNewX = TRUE; } } void CDlgCalcDlg::OnStore() { DisplayXRegister (); m_dblMemory = m_dblStack[0]; m_bStackLiftEnabled = TRUE; m_bNewX = TRUE; } void CDlgCalcDlg::OnRecall() { LiftStack (); m_dblStack[0] = m_dblMemory; DisplayXRegister (); m_bStackLiftEnabled = TRUE; m_bNewX = TRUE; } void CDlgCalcDlg::OnFix() { m_bFixPending = TRUE; } void CDlgCalcDlg::OnClear() { if (m_bFixPending) { m_bFixPending = FALSE; return; } m_bErrorFlag = FALSE; m_dblStack[0] = 0.0; DisplayXRegister (); m_bStackLiftEnabled = FALSE; m_bNewX = TRUE; } void CDlgCalcDlg::OnDecimal() { if (m_bNewX) { m_bNewX = FALSE; if (m_bStackLiftEnabled) { m_bStackLiftEnabled = FALSE; LiftStack (); } m_bDecimalInString = FALSE; m_strDisplay.Empty (); } int nLength = m_strDisplay.GetLength (); if ((nLength == MAXCHARS) ¦¦ (m_bDecimalInString)) ::MessageBeep (MB_ICONASTERISK); else { m_bDecimalInString = TRUE; m_strDisplay += (char) 0x2E; UpdateDisplay (m_strDisplay); m_dblStack[0] = strtod (m_strDisplay.GetBuffer (0), NULL); } } void CDlgCalcDlg::OnDelete() { int nLength = m_strDisplay.GetLength (); if (!m_bNewX && (nLength != 0)) { if (m_strDisplay[nLength - 1] == _T (`.')) m_bDecimalInString = FALSE; m_strDisplay = m_strDisplay.Left (nLength - 1); UpdateDisplay (m_strDisplay); m_dblStack[0] = strtod (m_strDisplay.GetBuffer (0), NULL); } } void CDlgCalcDlg::LiftStack() { for (int i=3; i>0; i--) m_dblStack[i] = m_dblStack[i-1]; } void CDlgCalcDlg::DropStack() { for (int i=1; i<3; i++) m_dblStack[i] = m_dblStack[i+1]; } void CDlgCalcDlg::DisplayXRegister() { double dblVal = m_dblStack[0]; if ((dblVal >= 1000000000000.0) ¦¦ (dblVal <= -1000000000000.0)) { UpdateDisplay (CString (_T ("Overflow error"))); m_bErrorFlag = TRUE; MessageBeep (MB_ICONASTERISK); } else { m_strDisplay.Format (m_strFormat, dblVal); UpdateDisplay (m_strDisplay); } } void CDlgCalcDlg::UpdateDisplay(LPCTSTR pszDisplay) { CClientDC dc (this); CFont* pOldFont = dc.SelectObject (GetFont ()); CSize size = dc.GetTextExtent (pszDisplay); CRect rect = m_rect; rect.InflateRect (-2, -2); int x = rect.right - size.cx - m_cxChar; int y = rect.top + ((rect.Height () - m_cyChar) / 2); dc.ExtTextOut (x, y, ETO_OPAQUE, rect, pszDisplay, NULL); dc.SelectObject (pOldFont); } |
By default, the main window in a dialog-based application created by AppWizard doesn't have a minimize button. I added one to the title bar by opening the dialog box in the dialog editor and checking Minimize Button in the dialog's property sheet.
The bulk of the code in DlgCalcDlg.cpp is there to process clicks of the calculator buttons. Thanks to this code, DlgCalc works very much like a genuine RPN calculator. To add 2 and 2, for example, you would type
2 <Enter> 2 + |
To multiply 3.46 by 9, add 13, divide by 10, and raise the result to a power of 2.5, you would type
3.46 <Enter> 9 * 13 + 10 / 2.5 <Exp> |
The Sto key copies the number in the calculator display to memory (stores it), and Rcl recalls it. Clx clears the calculator display (the "x" in "Clx" is a reference to the calculator's X register, whose contents are always shown in the calculator display), and the ± button changes the sign of the number that's currently displayed. Fix sets the number of digits displayed to the right of the decimal point. To change from two decimal places to four, click Fix and then the 4 button. The Del button deletes the rightmost character in the numeric display. For each button on the face of the calculator, there is an equivalent key on the keyboard, as shown in the following table. The P key assigned to the ± button is a crude mnemonic for "plus or minus." Most users find it slow going to click calculator buttons with the mouse, so the keyboard shortcuts are an important part of this application's user interface.
Keyboard Equivalents for DlgCalc's Calculator Buttons
Button(s) | Key(s) |
---|---|
± | P |
Exp | E |
Sto | S |
Rcl | R |
Enter | Enter |
Fix | F |
Clx | C |
0-9 | 0-9 |
- | - |
+ | + |
x | * |
÷ | / |
. | . |
Del | Del, Backspace |
Processing Keyboard Messages
Because it's unusual for a dialog box to implement its own keyboard interface on top of the one that Windows provides, DlgCalc's keyboard processing logic deserves a closer look.
A fundamental problem with processing keystrokes in a dialog box is that WM_CHAR messages are processed by ::IsDialogMessage, which is called from every MFC dialog's message loop. You can add an OnChar handler to a dialog class, but it will never get called if ::IsDialogMessage sees keyboard messages before ::TranslateMessage does. Another problem is that once a control gets the input focus, subsequent keyboard messages go to the control instead of to the dialog window.
To circumvent these problems, I decided to use accelerators to process keyboard input. I first created an accelerator resource by selecting the Resource command from Visual C++'s Insert menu and double-clicking "Accelerator." Then I added accelerators for all the keys on the face of the calculator—"1" for the IDC_1 button, "2" for the IDC_2 button, and so on. Next I added an HACCEL member variable to CDlgCalcDlg and inserted the following statement into CDlgCalcDlg's constructor to load the accelerators:
m_hAccel = ::LoadAccelerators (AfxGetInstanceHandle (), MAKEINTRESOURCE (IDR_ACCELL)); |
Finally, I overrode PreTranslateMessage and replaced it with a version that calls ::TranslateAccelerator on each message that the dialog receives:
BOOL CCalcDialog::PreTranslateMessage (MSG* pMsg) { if (m_hAccel != NULL) if (::TranslateAccelerator (m_hWnd, m_hAccel, pMsg)) return TRUE; return CDialog::PreTranslateMessage (pMsg); } |
This way, ::TranslateAccelerator sees keyboard messages even before ::IsDialogMessage does, and messages corresponding to accelerator keys are magically transformed into WM_COMMAND messages. Because the accelerator keys are assigned the same command IDs as the calculator's push buttons, the same ON_BN_CLICKED handlers process button clicks and keypresses.
Preprocessing WM_COMMAND Messages
Before a WM_COMMAND message emanating from a control is routed through a class's message map, MFC calls the class's virtual OnCommand function. The default implementation of OnCommand is the starting point for a command routing system put in place to ensure that all relevant objects associated with a running application program, including the document, view, and application objects used in document/view applications, see the message and get a crack at processing it. If desired, an application can preprocess WM_COMMAND messages by overriding OnCommand. When preprocessing is complete, the application can call the base class's OnCommand function to pass the message on for normal processing, or it can "eat" the message by returning without calling the base class. An OnCommand handler that doesn't call the base class should return TRUE to inform Windows that message processing is complete.
DlgCalc does something else unusual for an MFC application: it overrides OnCommand and filters out selected WM_COMMAND messages if either one of a pair of CDlgCalcDlg member variables—m_bErrorFlag or m_bFixPending—is nonzero. CDlgCalcDialog::OnCommand begins by obtaining the ID of the control that generated the message from the low word of the wParam value passed to it by MFC:
int nID = (int) LOWORD (wParam); |
It then examines m_bErrorFlag, which, if nonzero, indicates that a divide-by-zero or other error has occurred. The user must click Clx to clear the display after an error occurs, so OnCommand rejects all buttons but Clx if m_bErrorFlag is nonzero:
if (m_bErrorFlag && (nID != IDC_CLX)) { ::MessageBeep (MB_ICONASTERISK); return TRUE; } |
Similarly, if the m_bFixPending flag is set, indicating that the calculator is awaiting a press of a numeric key following a press of the Fix key, all buttons other than 0 through 9 and the Clx key, which cancels a pending fix operation, are rejected:
if (m_bFixPending && ((nID < IDC_0) ¦¦ (nID > IDC_9)) && (nID != IDC_CLX)) { ::MessageBeep (MB_ICONASTERISK); return TRUE; } |
In both cases, the ::MessageBeep API function is called to produce an audible tone signifying an invalid button press. The base class's OnCommand handler is called only if m_bErrorFlag and m_bFixPending are both 0. Putting the code that tests these flags in the OnCommand handler prevents the code from having to be duplicated in every ON_BN_CLICKED handler.
Another item of interest related to WM_COMMAND messages is the fact that DlgCalc processes clicks of the 0 through 9 buttons with a common handler. An ON_CONTROL_RANGE statement hand-coded into the message map directs BN_CLICKED notifications from each of the 10 buttons to CDlgCalcDlg::OnDigit:
ON_CONTROL_RANGE (BN_CLICKED, IDC_0, IDC_9, OnDigit) |
An ON_CONTROL_RANGE handler receives a UINT parameter identifying the control that sent the notification, and it returns void. In DlgCalc's case, the alternative to ON_CONTROL_RANGE would have been 10 separate ON_BN_CLICKED macros and a handler that called CWnd::GetCurrentMessage to retrieve the control ID from the message's wParam. One message-map entry is obviously more memory-efficient than ten, and the job of extracting control IDs from message parameters is best left to MFC when possible to ensure compatibility with future versions of Windows.