The Clock Application
The Clock application shown in Figure 14-1 uses a timer set to fire at 1-second intervals to periodically redraw a set of clock hands simulating an analog clock. Clock isn't a document/view application; it uses the MFC 1.0_style application architecture described in the first few chapters of this book. All of its source code, including the RC file, was generated by hand. (See Figure 14-2.) Besides demonstrating how to use a timer in a Windows application, Clock introduces a new MFC class named CTime and a new message, WM_MINMAXINFO. It also has several other interesting features that have nothing to do with timers, including these:
A command in the system menu for removing the window's title bar
A command in the system menu for making Clock's window a topmost window—one that's drawn on top of other windows even when it's running in the background
A persistent frame window that remembers its size and position
A frame window that can be dragged by its client area
We'll go over these and other unique aspects of the application in the sections that follow.
Figure 14-1. The Clock window.
Figure 14-2. The Clock application.
Resource.h#define IDM_SYSMENU_FULL_WINDOW 16
#define IDM_SYSMENU_STAY_ON_TOP 32
#define IDI_APPICON 100
Clock.rc#include <afxres.h>
#include "Resource.h"
IDI_APPICON ICON Clock.ico
Clock.hclass CMyApp : public CWinApp
{
public:
virtual BOOL InitInstance ();
};
class CMainWindow : public CFrameWnd
{
protected:
BOOL m_bFullWindow;
BOOL m_bStayOnTop;
int m_nPrevSecond;
int m_nPrevMinute;
int m_nPrevHour;
void DrawClockFace (CDC* pDC);
void DrawSecondHand (CDC* pDC, int nLength, int nScale, int nDegrees,
COLORREF clrColor);
void DrawHand (CDC* pDC, int nLength, int nScale, int nDegrees,
COLORREF clrColor);
void SetTitleBarState ();
void SetTopMostState ();
void SaveWindowState ();
void UpdateSystemMenu (CMenu* pMenu);
public:
CMainWindow ();
virtual BOOL PreCreateWindow (CREATESTRUCT& cs);
BOOL RestoreWindowState ();
protected:
afx_msg int OnCreate (LPCREATESTRUCT lpcs);
afx_msg void OnGetMinMaxInfo (MINMAXINFO* pMMI);
afx_msg void OnTimer (UINT nTimerID);
afx_msg void OnPaint ();
afx_msg UINT OnNcHitTest (CPoint point);
afx_msg void OnSysCommand (UINT nID, LPARAM lParam);
afx_msg void OnContextMenu (CWnd* pWnd, CPoint point);
afx_msg void OnEndSession (BOOL bEnding);
afx_msg void OnClose ();
DECLARE_MESSAGE_MAP ()
};
Clock.cpp#include <afxwin.h>
#include <math.h>
#include "Clock.h"
#include "Resource.h"
#define SQUARESIZE 20
#define ID_TIMER_CLOCK 1
CMyApp myApp;
/////////////////////////////////////////////////////////////////////////
// CMyApp member functions
BOOL CMyApp::InitInstance ()
{
SetRegistryKey (_T ("Programming Windows with MFC"));
m_pMainWnd = new CMainWindow;
if (!((CMainWindow*) m_pMainWnd)->RestoreWindowState ())
m_pMainWnd->ShowWindow (m_nCmdShow);
m_pMainWnd->UpdateWindow ();
return TRUE;
}
/////////////////////////////////////////////////////////////////////////
// CMainWindow message map and member functions
BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd)
ON_WM_CREATE ()
ON_WM_PAINT ()
ON_WM_TIMER ()
ON_WM_GETMINMAXINFO ()
ON_WM_NCHITTEST ()
ON_WM_SYSCOMMAND ()
ON_WM_CONTEXTMENU ()
ON_WM_ENDSESSION ()
ON_WM_CLOSE ()
END_MESSAGE_MAP ()
CMainWindow::CMainWindow ()
{
m_bAutoMenuEnable = FALSE;
CTime time = CTime::GetCurrentTime ();
m_nPrevSecond = time.GetSecond ();
m_nPrevMinute = time.GetMinute ();
m_nPrevHour = time.GetHour () % 12;
CString strWndClass = AfxRegisterWndClass (
CS_HREDRAW œ CS_VREDRAW,
myApp.LoadStandardCursor (IDC_ARROW),
(HBRUSH) (COLOR_3DFACE + 1),
myApp.LoadIcon (IDI_APPICON) );
Create (strWndClass, _T ("Clock"));
}
BOOL CMainWindow::PreCreateWindow (CREATESTRUCT& cs)
{
if (!CFrameWnd::PreCreateWindow (cs))
return FALSE;
cs.dwExStyle &= ~WS_EX_CLIENTEDGE;
return TRUE;
}
int CMainWindow::OnCreate (LPCREATESTRUCT lpcs)
{
if (CFrameWnd::OnCreate (lpcs) == -1)
return -1;
//
// Set a timer to fire at 1-second intervals.
//
if (!SetTimer (ID_TIMER_CLOCK, 1000, NULL)) {
MessageBox (_T ("SetTimer failed"), _T ("Error"),
MB_ICONSTOP œ MB_OK);
return -1;
}
//
// Customize the system menu.
//
CMenu* pMenu = GetSystemMenu (FALSE);
pMenu->AppendMenu (MF_SEPARATOR);
pMenu->AppendMenu (MF_STRING, IDM_SYSMENU_FULL_WINDOW,
_T ("Remove &Title"));
pMenu->AppendMenu (MF_STRING, IDM_SYSMENU_STAY_ON_TOP,
_T ("Stay on To&p"));
return 0;
}
void CMainWindow::OnClose ()
{
SaveWindowState ();
KillTimer (ID_TIMER_CLOCK);
CFrameWnd::OnClose ();
}
void CMainWindow::OnEndSession (BOOL bEnding)
{
if (bEnding)
SaveWindowState ();
CFrameWnd::OnEndSession (bEnding);
}
void CMainWindow::OnGetMinMaxInfo (MINMAXINFO* pMMI)
{
pMMI->ptMinTrackSize.x = 120;
pMMI->ptMinTrackSize.y = 120;
}
UINT CMainWindow::OnNcHitTest (CPoint point)
{
UINT nHitTest = CFrameWnd::OnNcHitTest (point);
if ((nHitTest == HTCLIENT) && (::GetAsyncKeyState (MK_LBUTTON) < 0))
nHitTest = HTCAPTION;
return nHitTest;
}
void CMainWindow::OnSysCommand (UINT nID, LPARAM lParam)
{
UINT nMaskedID = nID & 0xFFF0;
if (nMaskedID == IDM_SYSMENU_FULL_WINDOW) {
m_bFullWindow = m_bFullWindow ? 0 : 1;
SetTitleBarState ();
return;
}
else if (nMaskedID == IDM_SYSMENU_STAY_ON_TOP) {
m_bStayOnTop = m_bStayOnTop ? 0 : 1;
SetTopMostState ();
return;
}
CFrameWnd::OnSysCommand (nID, lParam);
}
void CMainWindow::OnContextMenu (CWnd* pWnd, CPoint point)
{
CRect rect;
GetClientRect (&rect);
ClientToScreen (&rect);
if (rect.PtInRect (point)) {
CMenu* pMenu = GetSystemMenu (FALSE);
UpdateSystemMenu (pMenu);
int nID = (int) pMenu->TrackPopupMenu (TPM_LEFTALIGN œ
TPM_LEFTBUTTON œ TPM_RIGHTBUTTON œ TPM_RETURNCMD, point.x,
point.y, this);
if (nID > 0)
SendMessage (WM_SYSCOMMAND, nID, 0);
return;
}
CFrameWnd::OnContextMenu (pWnd, point);
}
void CMainWindow::OnTimer (UINT nTimerID)
{
//
// Do nothing if the window is minimized.
//
if (IsIconic ())
return;
//
// Get the current time and do nothing if it hasn't changed.
//
CTime time = CTime::GetCurrentTime ();
int nSecond = time.GetSecond ();
int nMinute = time.GetMinute ();
int nHour = time.GetHour () % 12;
if ((nSecond == m_nPrevSecond) &&
(nMinute == m_nPrevMinute) &&
(nHour == m_nPrevHour))
return;
//
// Center the origin and switch to the MM_ISOTROPIC mapping mode.
//
CRect rect;
GetClientRect (&rect);
CClientDC dc (this);
dc.SetMapMode (MM_ISOTROPIC);
dc.SetWindowExt (1000, 1000);
dc.SetViewportExt (rect.Width (), -rect.Height ());
dc.SetViewportOrg (rect.Width () / 2, rect.Height () / 2);
//
// If minutes have changed, erase the hour and minute hands.
//
COLORREF clrColor = ::GetSysColor (COLOR_3DFACE);
if (nMinute != m_nPrevMinute) {
DrawHand (&dc, 200, 4, (m_nPrevHour * 30) + (m_nPrevMinute / 2),
clrColor);
DrawHand (&dc, 400, 8, m_nPrevMinute * 6, clrColor);
m_nPrevMinute = nMinute;
m_nPrevHour = nHour;
}
//
// If seconds have changed, erase the second hand and redraw all hands.
//
if (nSecond != m_nPrevSecond) {
DrawSecondHand (&dc, 400, 8, m_nPrevSecond * 6, clrColor);
DrawSecondHand (&dc, 400, 8, nSecond * 6, RGB (0, 0, 0));
DrawHand (&dc, 200, 4, (nHour * 30) + (nMinute / 2),
RGB (0, 0, 0));
DrawHand (&dc, 400, 8, nMinute * 6, RGB (0, 0, 0));
m_nPrevSecond = nSecond;
}
}
void CMainWindow::OnPaint ()
{
CRect rect;
GetClientRect (&rect);
CPaintDC dc (this);
dc.SetMapMode (MM_ISOTROPIC);
dc.SetWindowExt (1000, 1000);
dc.SetViewportExt (rect.Width (), -rect.Height ());
dc.SetViewportOrg (rect.Width () / 2, rect.Height () / 2);
DrawClockFace (&dc);
DrawHand (&dc, 200, 4, (m_nPrevHour * 30) +
(m_nPrevMinute / 2), RGB (0, 0, 0));
DrawHand (&dc, 400, 8, m_nPrevMinute * 6, RGB (0, 0, 0));
DrawSecondHand (&dc, 400, 8, m_nPrevSecond * 6, RGB (0, 0, 0));
}
void CMainWindow::DrawClockFace (CDC* pDC)
{
static CPoint point[12] = {
CPoint ( 0, 450), // 12 o'clock
CPoint ( 225, 390), // 1 o'clock
CPoint ( 390, 225), // 2 o'clock
CPoint ( 450, 0), // 3 o'clock
CPoint ( 390, -225), // 4 o'clock
CPoint ( 225, -390), // 5 o'clock
CPoint ( 0, -450), // 6 o'clock
CPoint (-225, -390), // 7 o'clock
CPoint (-390, -225), // 8 o'clock
CPoint (-450, 0), // 9 o'clock
CPoint (-390, 225), // 10 o'clock
CPoint (-225, 390), // 11 o'clock
};
pDC->SelectStockObject (NULL_BRUSH);
for (int i=0; i<12; i++)
pDC->Rectangle (point[i].x - SQUARESIZE,
point[i].y + SQUARESIZE, point[i].x + SQUARESIZE,
point[i].y - SQUARESIZE);
}
void CMainWindow::DrawHand (CDC* pDC, int nLength, int nScale,
int nDegrees, COLORREF clrColor)
{
CPoint point[4];
double nRadians = (double) nDegrees * 0.017453292;
point[0].x = (int) (nLength * sin (nRadians));
point[0].y = (int) (nLength * cos (nRadians));
point[2].x = -point[0].x / nScale;
point[2].y = -point[0].y / nScale;
point[1].x = -point[2].y;
point[1].y = point[2].x;
point[3].x = -point[1].x;
point[3].y = -point[1].y;
CPen pen (PS_SOLID, 0, clrColor);
CPen* pOldPen = pDC->SelectObject (&pen);
pDC->MoveTo (point[0]);
pDC->LineTo (point[1]);
pDC->LineTo (point[2]);
pDC->LineTo (point[3]);
pDC->LineTo (point[0]);
pDC->SelectObject (pOldPen);
}
void CMainWindow::DrawSecondHand (CDC* pDC, int nLength, int nScale,
int nDegrees, COLORREF clrColor)
{
CPoint point[2];
double nRadians = (double) nDegrees * 0.017453292;
point[0].x = (int) (nLength * sin (nRadians));
point[0].y = (int) (nLength * cos (nRadians));
point[1].x = -point[0].x / nScale;
point[1].y = -point[0].y / nScale;
CPen pen (PS_SOLID, 0, clrColor);
CPen* pOldPen = pDC->SelectObject (&pen);
pDC->MoveTo (point[0]);
pDC->LineTo (point[1]);
pDC->SelectObject (pOldPen);
}
void CMainWindow::SetTitleBarState ()
{
CMenu* pMenu = GetSystemMenu (FALSE);
if (m_bFullWindow ) {
ModifyStyle (WS_CAPTION, 0);
pMenu->ModifyMenu (IDM_SYSMENU_FULL_WINDOW, MF_STRING,
IDM_SYSMENU_FULL_WINDOW, _T ("Restore &Title"));
}
else {
ModifyStyle (0, WS_CAPTION);
pMenu->ModifyMenu (IDM_SYSMENU_FULL_WINDOW, MF_STRING,
IDM_SYSMENU_FULL_WINDOW, _T ("Remove &Title"));
}
SetWindowPos (NULL, 0, 0, 0, 0, SWP_NOMOVE œ SWP_NOSIZE œ
SWP_NOZORDER œ SWP_DRAWFRAME);
}
void CMainWindow::SetTopMostState ()
{
CMenu* pMenu = GetSystemMenu (FALSE);
if (m_bStayOnTop) {
SetWindowPos (&wndTopMost, 0, 0, 0, 0, SWP_NOMOVE œ SWP_NOSIZE);
pMenu->CheckMenuItem (IDM_SYSMENU_STAY_ON_TOP, MF_CHECKED);
}
else {
SetWindowPos (&wndNoTopMost, 0, 0, 0, 0, SWP_NOMOVE œ SWP_NOSIZE);
pMenu->CheckMenuItem (IDM_SYSMENU_STAY_ON_TOP, MF_UNCHECKED);
}
}
BOOL CMainWindow::RestoreWindowState ()
{
CString version = _T ("Version 1.0");
m_bFullWindow = myApp.GetProfileInt (version, _T ("FullWindow"), 0);
SetTitleBarState ();
m_bStayOnTop = myApp.GetProfileInt (version, _T ("StayOnTop"), 0);
SetTopMostState ();
WINDOWPLACEMENT wp;
wp.length = sizeof (WINDOWPLACEMENT);
GetWindowPlacement (&wp);
if (((wp.flags =
myApp.GetProfileInt (version, _T ("flags"), -1)) != -1) &&
((wp.showCmd =
myApp.GetProfileInt (version, _T ("showCmd"), -1)) != -1) &&
((wp.rcNormalPosition.left =
myApp.GetProfileInt (version, _T ("x1"), -1)) != -1) &&
((wp.rcNormalPosition.top =
myApp.GetProfileInt (version, _T ("y1"), -1)) != -1) &&
((wp.rcNormalPosition.right =
myApp.GetProfileInt (version, _T ("x2"), -1)) != -1) &&
((wp.rcNormalPosition.bottom =
myApp.GetProfileInt (version, _T ("y2"), -1)) != -1)) {
wp.rcNormalPosition.left = min (wp.rcNormalPosition.left,
::GetSystemMetrics (SM_CXSCREEN) -
::GetSystemMetrics (SM_CXICON));
wp.rcNormalPosition.top = min (wp.rcNormalPosition.top,
::GetSystemMetrics (SM_CYSCREEN) -
::GetSystemMetrics (SM_CYICON));
SetWindowPlacement (&wp);
return TRUE;
}
return FALSE;
}
void CMainWindow::SaveWindowState ()
{
CString version = _T ("Version 1.0");
myApp.WriteProfileInt (version, _T ("FullWindow"), m_bFullWindow);
myApp.WriteProfileInt (version, _T ("StayOnTop"), m_bStayOnTop);
WINDOWPLACEMENT wp;
wp.length = sizeof (WINDOWPLACEMENT);
GetWindowPlacement (&wp);
myApp.WriteProfileInt (version, _T ("flags"), wp.flags);
myApp.WriteProfileInt (version, _T ("showCmd"), wp.showCmd);
myApp.WriteProfileInt (version, _T ("x1"), wp.rcNormalPosition.left);
myApp.WriteProfileInt (version, _T ("y1"), wp.rcNormalPosition.top);
myApp.WriteProfileInt (version, _T ("x2"), wp.rcNormalPosition.right);
myApp.WriteProfileInt (version, _T ("y2"), wp.rcNormalPosition.bottom);
}
void CMainWindow::UpdateSystemMenu (CMenu* pMenu)
{
static UINT nState[2][5] = {
{ MFS_GRAYED, MFS_ENABLED, MFS_ENABLED,
MFS_ENABLED, MFS_DEFAULT },
{ MFS_DEFAULT, MFS_GRAYED, MFS_GRAYED,
MFS_ENABLED, MFS_GRAYED }
};
if (IsIconic ()) // Shouldn't happen, but let's be safe
return;
int i = 0;
if (IsZoomed ())
i = 1;
CString strMenuText;
pMenu->GetMenuString (SC_RESTORE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_RESTORE, MF_STRING œ nState[i][0], SC_RESTORE,
strMenuText);
pMenu->GetMenuString (SC_MOVE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_MOVE, MF_STRING œ nState[i][1], SC_MOVE,
strMenuText);
pMenu->GetMenuString (SC_SIZE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_SIZE, MF_STRING œ nState[i][2], SC_SIZE,
strMenuText);
pMenu->GetMenuString (SC_MINIMIZE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_MINIMIZE, MF_STRING œ nState[i][3], SC_MINIMIZE,
strMenuText);
pMenu->GetMenuString (SC_MAXIMIZE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_MAXIMIZE, MF_STRING œ nState[i][4], SC_MAXIMIZE,
strMenuText);
SetMenuDefaultItem (pMenu->m_hMenu, i ? SC_RESTORE :
SC_MAXIMIZE, FALSE);
}
Processing Timer Messages
Clock uses SetTimer to program a timer in OnCreate. The timer is destroyed in OnClose with KillTimer. When a WM_TIMER message arrives, CMainWindow::OnTimer gets the current time and compares the hour, minute, and second to the hour, minute, and second previously recorded in the member variables m_nPrevHour, m_nPrevMinute, and m_nPrevSecond. If the current hour, minute, and second equal the hour, minute, and second recorded earlier, OnTimer does nothing. Otherwise, it records the new time and moves the clock hands. CMainWindow::DrawHand draws the hour and minute hands, and CMainWindow::DrawSecondHand draws the second hand. A hand is moved by calling the corresponding drawing function twice: once to erase the hand by drawing over it with the window background color (COLOR_3DFACE) and once to draw the hand—in black—in its new position.
With this OnTimer mechanism in place, the clock's second hand is moved roughly once per second and the hour and minute hands are moved whenever the current number of minutes past the hour no longer equals the previously recorded minutes-past-the-hour. Because the hands are drawn to reflect the current time and not some assumed time based on the number of WM_TIMER messages received, it doesn't matter if WM_TIMER messages are skipped as the window is dragged or resized. If you watch closely, you'll see that the second hand occasionally advances by two seconds rather than one. That's because every now and then a WM_TIMER message arrives just before a new second ticks off and the next WM_TIMER message arrives a split second after the next new second. You could prevent this from happening by lowering the timer interval to, say, 0.5 second. The cost would be more overhead to the system, but the added overhead would be minimal because OnTimer is structured so that it redraws the clock hands (by far the most labor-intensive part of the process) only if the time has changed since the last timer message.
Before doing anything else, OnTimer calls the main window's IsIconic function to determine whether the window is currently minimized. IsIconic returns nonzero for a minimized window and 0 for an unminimized window. (A complementary function, CWnd::IsZoomed, returns a nonzero value if a window is maximized and 0 if it isn't.) If IsIconic returns nonzero, OnTimer exits immediately to prevent the clock display from being updated when the window isn't displayed. When a minimized window calls GetClientRect in Windows 95 or higher or Windows NT 4.0 or higher, the returned rectangle is a NULL rectangle—one whose coordinates equal 0. The application can try to paint in this rectangle, but the GDI will clip the output. Checking for a minimized window upon each timer tick reduces the load on the CPU by eliminating unnecessary drawing.
If you'd rather that Clock not sit idle while its window is minimized, try rewriting the beginning of the OnTimer function so that it looks like this:
CTime time = CTime::GetCurrentTime ();
int nSecond = time.GetSecond ();
int nMinute = time.GetMinute ();
int nHour = time.GetHour () % 12;
if (IsIconic ()) {
CString time;
time.Format (_T ("%0.2d:%0.2d:%0.2d"), nHour, nMinute, nSecond);
SetWindowText (time);
return;
}
else {
SetWindowText (_T ("Clock"));
}
An application can change the text displayed next to its icon in the taskbar by changing the window title with CWnd::SetWindowText. If modified as shown above, Clock will tick off the seconds in the taskbar while it is minimized.
Getting the Current Time:The CTime Class
To query the system for the current time, Clock uses a CTime object. CTime is an MFC class that represents times and dates. It includes convenient member functions for getting the date, time, day of the week (Sunday, Monday, Tuesday, and so on), and other information. Overloaded operators such as +, -, and > allow you to manipulate times and dates with the ease of simple integers.
The CTime member functions that interest us are GetCurrentTime, which is a static function that returns a CTime object initialized to the current date and time; GetHour, which returns the hour (0 through 23); GetMinute, which returns the number of minutes past the hour (0 through 59); and GetSecond, which returns the number of seconds (0 through 59). OnTimer uses the following statements to retrieve the current hour, minute, and second so that it can determine whether the clock display needs to be updated:
CTime time = CTime::GetCurrentTime ();
int nSecond = time.GetSecond ();
int nMinute = time.GetMinute ();
int nHour = time.GetHour () % 12;
The modulo-12 operation applied to GetHour's return value converts the hour to an integer from 0 through 11. CMainWindow's constructor uses similar code to initialize m_nPrevHour, m_nPrevMinute, and m_nPrevSecond.
Using the MM_ISOTROPIC Mapping Mode
Up to now, most of the applications that we've developed have used the default MM_TEXT mapping mode. The mapping mode governs how Windows converts the logical units passed to CDC drawing functions into device units (pixels) on the display. In the MM_TEXT mapping mode, logical units and device units are one and the same, so if an application draws a line from (0,0) to (50,100), the line extends from the pixel in the upper left corner of the display surface to the pixel that lies 50 pixels to the right of and 100 pixels below the upper left corner. This assumes, of course, that the drawing origin hasn't been moved from its default location in the upper left corner of the display surface.
MM_TEXT is fine for most applications, but you can use other GDI mapping modes to lessen an application's dependency on the physical characteristics of the display. (For a review of GDI mapping modes, refer to Chapter 2.) In the MM_LOENGLISH mapping mode, for example, one logical unit is equal to 1/100 of an inch, so if you want to draw a line exactly 1 inch long, you can use a length of 100 units and Windows will factor in the number of pixels per inch when it scan-converts the line into pixels. The conversion might not be perfect for screen DCs because Windows uses assumed pixel-per-inch values for screens that aren't based on the physical screen size. Windows can obtain precise pixel-per-inch values for printers and other hardcopy devices, however, so by using MM_LOENGLISH for printer output, you really can draw a line 1 inch long.
Clock uses the MM_ISOTROPIC mapping mode, in which logical units measured along the x axis have the same physical dimensions as logical units measured along the y axis. Before drawing the clock's face and hands in response to a WM_TIMER or WM_PAINT message, Clock measures the window's client area with GetClientRect and creates a device context. Then it sets the mapping mode to MM_ISOTROPIC, moves the origin of the coordinate system so that the logical point (0,0) lies at the center of the window's client area, and sets the window extents so that the window's client area measures 1,000 logical units in each direction. Here's how it looks in code:
CRect rect;
GetClientRect (&rect);
CClientDC dc (this); // In OnPaint, use CPaintDC instead.
dc.SetMapMode (MM_ISOTROPIC);
dc.SetWindowExt (1000, 1000);
dc.SetViewportExt (rect.Width (), -rect.Height ());
dc.SetViewportOrg (rect.Width () / 2, rect.Height () / 2);
The negative value passed to SetViewportExt specifying the viewport's physical height orients the coordinate system such that values of y increase in the upward direction. If the negative sign were omitted, increasing values of y would move down the screen rather than up because Windows numbers pixels at the bottom of the screen higher than it does pixels at the top. Figure 14-3 shows what the coordinate system looks like after it is transformed. The coordinate system is centered in the window's client area, and values of x and y increase as you move to the right and up. The result is a four-quadrant Cartesian coordinate system that happens to be a very convenient model for drawing an analog clock face.
Figure 14-3. Clock's coordinate system for screen output.
Once you've configured the coordinate system this way, you can write the routines that draw the clock's face and hands without regard for the physical dimensions of the window. When DrawHand is called to draw a clock hand, the length value passed in the second parameter is either 200 for an hour hand or 400 for a minute hand. DrawSecondHand, too, is passed a length of 400. Because the distance from the origin of the coordinate system to any edge of the window is 500 logical units, the minute and second hands extend outward 80 percent of the distance to the nearest window edge and the hour hand spans 40 percent of the distance. If you used the MM_TEXT mapping mode instead, you'd have to scale every coordinate and every distance manually before passing it to the GDI.
Hiding and Displaying the Title Bar
Clock's system menu contains two extra commands: Remove Title and Stay On Top. Remove Title removes the window's title bar so that the clock face fills the entire window. You can restore the title bar by displaying the system menu again and selecting Restore Title, which appears where Remove Title used to be. The magic underlying this transformation is simple, yet adding or removing a title bar on the fly is enough to make even seasoned Windows programmers scratch their heads in bewilderment the first time they try it.
The secret lies in CMainWindow::SetTitleBarState. The attribute that determines whether a window has a title bar is the WS_CAPTION style bit, which is included in the WS_OVERLAPPEDWINDOW style used by most frame windows. Creating a window without a title bar is as simple as omitting WS_CAPTION from the window style. It follows that you can remove a title bar from a window that already exists by stripping the WS_CAPTION bit. MFC's CWnd::ModifyStyle function changes a window's style with one simple function call. When Remove/Restore Title is selected from Clock's system menu, CMainWindow::OnSysCommand toggles the value stored in CMainWindow::m_bFullWindow from 0 to 1 or 1 to 0 and then calls CMainWindow::SetTitleBarState, which adds or removes the WS_CAPTION style based on the current value of m_bFullWindow:
if (m_bFullWindow ) {
ModifyStyle (WS_CAPTION, 0);
pMenu->ModifyMenu (IDM_SYSMENU_FULL_WINDOW, MF_STRING,
IDM_SYSMENU_FULL_WINDOW, _T ("Restore &Title"));
}
else {
ModifyStyle (0, WS_CAPTION);
pMenu->ModifyMenu (IDM_SYSMENU_FULL_WINDOW, MF_STRING,
IDM_SYSMENU_FULL_WINDOW, _T ("Remove &Title"));
}
The first parameter passed to ModifyStyle specifies the style or styles to remove, and the second parameter specifies the style or styles to add. SetTitleBarState also sets the menu item text to match the state of the style bit: "Remove Title" if the title bar is displayed and "Restore Title" if it isn't.
Toggling WS_CAPTION on and off is only half the battle, however. The trick is getting the window's nonclient area to repaint once the window style is changed. Calling CWnd::Invalidate won't do it, but calling SetWindowPos with a SWP_DRAWFRAME parameter will:
SetWindowPos (NULL, 0, 0, 0, 0, SWP_NOMOVE ¦ SWP_NOSIZE ¦
SWP_NOZORDER ¦ SWP_DRAWFRAME);
The combination of SetWindowPos and SWP_DRAWFRAME causes the entire window, including the title bar, to redraw. The other SWP flags passed to SetWindowPos preserve the window's position, size, and position in the z-order—the front-to-back ordering of windows that determines which windows are painted on top of others.
Implementing Client-Area Drag
One problem with a window without a title bar is that it can't be repositioned with the mouse. Windows are dragged by their title bars, and when there's no title bar, the user has nothing to grab onto. Clock solves this little dilemma by playing a trick with the window's WM_NCHITTEST handler so that the window can be dragged by its client area, a feature Windows programmers call client-area drag.
In Windows, every mouse message is preceded by a WM_NCHITTEST message containing screen coordinates identifying the cursor location. The message is normally handled by ::DefWindowProc, which returns a code that tells Windows what part of the window the cursor is over. Windows uses the return value to decide what type of mouse message to send. For example, if the left mouse button is clicked over the window's title bar, ::DefWindowProc's WM_NCHITTEST handler returns HTCAPTION and Windows sends the window a WM_NCLBUTTONDOWN message. If ::DefWindowProc returns HTCLIENT instead, Windows converts the cursor coordinates from screen coordinates to client coordinates and passes them to the window in a WM_LBUTTONDOWN message.
The fact that an application sees mouse messages in raw form makes for some interesting possibilities. The following OnNcHitTest handler implements client-area drag by fooling Windows into thinking that the mouse is over the title bar when in fact it's over the window's client area:
UINT CMainWindow::OnNcHitTest (CPoint point)
{
UINT nHitTest = CFrameWnd::OnNcHitTest (point);
if (nHitTest == HTCLIENT)
nHitTest = HTCAPTION;
return nHitTest;
}
With this OnNcHitTest handler in place, a window is as easily dragged by its client area as by its title bar. And it works even if the window doesn't have a title bar. Try it: click the left mouse button in Clock's client area, and move the mouse with the button held down. The window should go wherever the mouse goes.
Clock uses an OnNcHitTest handler similar to the one shown above. The only difference is that Clock verifies that the left mouse button is down before replacing an HTCLIENT return code with HTCAPTION so that other mouse messages—particularly right-button mouse messages that precede WM_CONTEXTMENU messages—will get through unscathed:
UINT CMainWindow::OnNcHitTest (CPoint point)
{
UINT nHitTest = CFrameWnd::OnNcHitTest (point);
if ((nHitTest == HTCLIENT) &&
(::GetAsyncKeyState (MK_LBUTTON) < 0))
nHitTest = HTCAPTION;
return nHitTest;
}
The call to ::GetAsyncKeyState checks the left mouse button and returns a negative value if the button is currently down.
Using the System Menu as a Context Menu
Removing a window's title bar has other implications, too. Without a title bar, the user has nothing to click on to display the system menu so that the title bar can be restored. Clock's solution is an OnContextMenu handler that displays the system menu as a context menu when the right mouse button is clicked in the window's client area. Popping up a system menu at an arbitrary location is easier said than done because there's no convenient API function for displaying a system menu programmatically. Clock demonstrates one technique that you can use to do it yourself.
When Clock's client area is clicked with the right mouse button, CMainWindow's OnContextMenu handler retrieves a CMenu pointer to the system menu with GetSystemMenu and displays the menu with CMenu::TrackPopupMenu:
CMenu* pMenu = GetSystemMenu (FALSE);
int nID = (int) pMenu->TrackPopupMenu (TPM_LEFTALIGN ¦
TPM_LEFTBUTTON ¦ TPM_RIGHTBUTTON ¦ TPM_RETURNCMD, point.x,
point.y, this);
One problem with this solution is that commands selected from the menu produce WM_COMMAND messages instead of WM_SYSCOMMAND messages. To compensate, Clock passes TrackPopupMenu a TPM_RETURNCMD flag instructing it to return the ID of the selected menu item. If TrackPopupMenu returns a positive, nonzero value, indicating that an item was selected, Clock sends itself a WM_SYSCOMMAND message with wParam equal to the menu item ID as shown below.
if (nID > 0)
SendMessage (WM_SYSCOMMAND, nID, 0);
Consequently, OnSysCommand gets called to process selections from the pseudo_system menu just as it does for selections from the real system menu. To prevent the framework from disabling the items added to the system menu because of the lack of ON_COMMAND handlers, CMainWindow's constructor sets m_bAutoMenuEnable to FALSE. Normally, the framework's automatic enabling and disabling of menu items doesn't affect items added to the system menu, but Clock's system menu is an exception because it's treated as a conventional menu when it's displayed with TrackPopupMenu.
So far, so good. There's just one problem remaining. Windows interactively enables and disables certain commands in the system menu so that the selection of commands available is consistent with the window state. For example, the Move, Size, and Maximize commands are grayed in a maximized window's system menu but the Restore and Minimize commands are not. If the same window is restored to its unmaximized size, Restore is grayed out but all other commands are enabled. Unfortunately, when you get a menu pointer with GetSystemMenu, the menu items haven't been updated yet. Therefore, OnContextMenu calls a CMainWindow function named UpdateSystemMenu to manually update the menu item states based on the current state of the window. After UpdateSystemMenu updates the system menu by placing a series of calls to CMenu::GetMenuString and CMenu::ModifyMenu, it uses the ::SetMenuDefaultItem API function to set the default menu item (the one displayed in boldface type) to either Restore or Maximize, depending on the window state. UpdateSystemMenu is hardly an ideal solution, but it works, and to date I haven't found a better way to keep the items in a programmatically displayed system menu in sync with the window the menu belongs to.
Topmost Windows
One of the innovations Windows 3.1 introduced was the notion of a topmost window—a window whose position in the z-order is implicitly higher than those of conventional, or nontopmost, windows. Normally, the window at the top of the z-order is painted on top of other windows, the window that's second in the z-order is painted on top of windows other than the first, and so on. A topmost window, however, receives priority over other windows so that it's not obscured even if it's at the bottom of the z-order. It's always visible, even while it's running in the background.
The Windows taskbar is the perfect example of a topmost window. By default, the taskbar is designated a topmost window so that it will be drawn on top of other windows. If two (or more) topmost windows are displayed at the same time, the normal rules of z-ordering determine the visibility of each one relative to the other. You should use topmost windows sparingly because if all windows were topmost windows, a topmost window would no longer be accorded priority over other windows.
The difference between a topmost window and a nontopmost window is an extended window style bit. WS_EX_TOPMOST makes a window a topmost window. You can create a topmost frame window by including a WS_EX_TOPMOST flag in the call to Create, like this:
Create (NULL, _T ("MyWindow"), WS_OVERLAPPEDWINDOW, rectDefault,
NULL, NULL, WS_EX_TOPMOST);
The alternative is to add the style bit after the window is created by calling SetWindowPos with a &wndTopMost parameter, as shown here:
SetWindowPos (&wndTopMost, 0, 0, 0, 0, SWP_NOMOVE ¦ SWP_NOSIZE);
You can convert a topmost window into a nontopmost window by calling SetWindowPos with the first parameter equal to &wndNoTopMost rather than &wndTopMost.
Clock uses SetWindowPos to make its window a topmost window when Stay On Top is checked in the system menu and a nontopmost window when Stay On Top is unchecked. The work is done by CMainWindow::SetTopMostState, which is called by OnSysCommand. When Stay On Top is checked, Clock is visible on the screen at all times, even if it's running in the background and it overlaps the application running in the foreground.
Making Configuration Settings Persistent
Clock is the first program presented thus far that makes program settings persistent by recording them on disk. The word persistent comes up a lot in discussions of Windows programming. Saying that a piece of information is persistent means that it's preserved across sessions. If you want Clock to run in a tiny window in the lower right corner of your screen, you can size it and position it once and it will automatically come back up in the same size and position the next time it's started. For users who like to arrange their desktops a certain way, little touches like this one make the difference between a good application and a great one. Other Clock configuration settings are preserved, too, including the title bar and stay-on-top states.
The key to preserving configuration information across sessions is to store it on the hard disk so that it can be read back again the next time the application is started. In 16-bit Windows, applications commonly use ::WriteProfileString, ::GetProfileString, and other API functions to store configuration settings in Win.ini or private INI files. In 32-bit Windows, INI files are still supported for backward compatibility, but programmers are discouraged from using them. 32-bit applications should store configuration settings in the registry instead.
The registry is a binary database that serves as a central data repository for the operating system and the applications it hosts. Information stored in the registry is organized hierarchically using a system of keys and subkeys, which are analogous to directories and subdirectories on a hard disk. Keys can contain data entries just as directories can contain files. Data entries have names and can be assigned text or binary values. The uppermost level in the registry's hierarchy is a set of six root keys named HKEY_CLASSES_ROOT, HKEY_USERS, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_CURRENT_CONFIG, and HKEY_DYN_DATA. Per Microsoft's recommendations, Windows applications should store private configuration settings under the key
HKEY_CURRENT_USER\Software\CompanyName\ProductName\Version
where CompanyName is the company name, ProductName is the product name, and Version is the product's version number. A registry entry that records the user-selectable window background color for version 2.0 of a product named WidgetMaster from WinWidgets, Inc., might look like this:
HKEY_CURRENT_USER\Software\WinWidgets, Inc.\WidgetMaster\Version 2.0\BkgndColor=4
Because the information is stored under HKEY_CURRENT_USER, it is maintained on a per-user basis. That is, if another user logs in and runs the same application but selects a different background color, a separate BkgndColor value will be recorded for that user.
The Win32 API includes an assortment of functions for reading and writing to the registry, but MFC provides a layer on top of the API that makes reading and writing application-specific registry values no different from using ordinary INI files. A call to CWinApp::SetRegistryKey with the name of a registry key directs the framework to use the registry instead of an INI file. The key name passed to SetRegistryKey corresponds to the company name—for example, "WinWidgets, Inc." in the example above. String and numeric values are written to the registry with CWinApp's WriteProfileString and WriteProfileInt functions and read back with GetProfileString and GetProfileInt. In an application named MyWord.exe, the statements
SetRegistryKey (_T ("WordSmith"));
WriteProfileInt (_T ("Version 1.0"), _T ("MRULength"), 8);
create the following numeric registry entry:
HKEY_CURRENT_USER\Software\WordSmith\MYWORD\Version 1.0\MRULength=8
The statement
m_nMRULength = GetProfileInt (_T ("Version 1.0"), _T ("MRULength"), 4);
reads it back and returns 4 if the entry doesn't exist. Note that MFC generates the product name for you by stripping the .exe extension from the executable file name.
Before it terminates, Clock records the following configuration settings in the registry:
The value of CMainWindow::m_bFullWindow, which indicates whether the title bar is displayed
The value of CMainWindow::m_bStayOnTop, which indicates whether Stay On Top is selected
The size and position of the frame window
The next time it starts up, Clock reads the settings back. The full complement of entries that Clock stores in the registry is shown in Figure 14-4. The CMainWindow functions SaveWindowState and RestoreWindowState do the reading and writing. SaveWindowState is called from the window's OnClose and OnEndSession handlers, which are called just before the application closes and just before Windows shuts down, respectively. If Windows is shut down, a running application doesn't receive a WM_CLOSE message, but it does receive a WM_ENDSESSION message. If you want to know whether Windows is preparing to shut down, simply add an ON_WM_ENDSESSION entry to the main window's message map and write an OnEndSession handler to go with it. The bEnding parameter passed to OnEndSession indicates whether Windows is in fact shutting down. A nonzero value means it is; 0 means Windows was about to shut down but another application vetoed the operation. A WM_ENDSESSION message is preceded by a WM_QUERYENDSESSION message, in which each application is given a chance to say yes or no to an impending shutdown.
Figure 14-4. Clock's registry entries as seen in Registry Editor (RegEdit.exe).
Clock's title bar and stay-on-top settings are saved to the HKEY_CURRENT_USER\Software\Programming Windows with MFC\CLOCK\Version 1.0 branch of the registry with the following statements in SaveWindowState.
CString version = _T ("Version 1.0");
myApp.WriteProfileInt (version, _T ("FullWindow"), m_bFullWindow);
myApp.WriteProfileInt (version, _T ("StayOnTop"), m_bStayOnTop);
The settings are read back and applied to the window in RestoreWindowState:
CString version = _T ("Version 1.0");
m_bFullWindow = myApp.GetProfileInt (version, _T ("FullWindow"), 0);
SetTitleBarState ();
m_bStayOnTop = myApp.GetProfileInt (version, _T ("StayOnTop"), 0);
SetTopMostState ();
RestoreWindowState is called by CMyApp::InitInstance right after the window is created but before it's displayed on the screen.
Saving and restoring the window's size and position is a little trickier. If you've never written an application with a window that remembers its size and position, you might think it would be a simple matter of saving the coordinates returned by CWnd::GetWindowRect so that they can be passed to Create or CreateEx. But there's more to it than that. If you fail to take into account the window's current state (minimized, maximized, or neither minimized nor maximized), all kinds of bad things can happen. For example, if you pass the coordinates of a maximized window to Create or CreateEx, the resultant window will occupy the full extent of the screen but its title bar will have a maximize box instead of a restore box. A persistent window that's closed while it's minimized or maximized should come back up in the minimized or maximized state, and it should also remember its normal size so that restoring it will restore its former size.
The key to preserving a window's size and position and taking relevant state information into account lies in a pair of CWnd functions named GetWindowPlacement and SetWindowPlacement. Each accepts the address of a WINDOWPLACEMENT structure, which is defined as follows:
typedef struct tagWINDOWPLACEMENT {
UINT length;
UINT flags;
UINT showCmd;
POINT ptMinPosition;
POINT ptMaxPosition;
RECT rcNormalPosition;
} WINDOWPLACEMENT;
WINDOWPLACEMENT brings together in one place everything Windows needs to know to characterize a window's screen state. length specifies the size of the WINDOWPLACEMENT structure. Both CWnd::GetWindowPlacement and CWnd::SetWindowPlacement fill in this field for you. flags contains zero or more bit flags specifying information about minimized windows. The WPF_RESTORETOMAXIMIZED flag, if present, indicates that a minimized window will be maximized when it is restored. showCmd specifies the window's current display state. It is set to SW_SHOWMINIMIZED if the window is minimized, SW_SHOWMAXIMIZED if the window is maximized, or SW_SHOWNORMAL if the window is neither minimized nor maximized. ptMinPosition and ptMaxPosition hold the screen coordinates of the window's upper left corner when it is minimized and maximized, respectively. (Don't rely on ptMinPosition to tell you anything; in current versions of Windows, ptMinPosition is set to (3000,3000) when a window is minimized.) rcNormalPosition contains the screen coordinates of the window's "normal," or unminimized and unmaximized, screen position. When a window is minimized or maximized, rcNormalPosition specifies the position and size the window will be restored to—provided, of course, that the WPF_RESTORETOMAXIMIZED flag isn't set to force a restored window to full screen.
You can preserve a window's screen state across sessions by saving the flags, showCmd, and rcNormalPosition values in the window's WINDOWPLACEMENT structure and restoring these values when the window is re-created. You don't need to save ptMinPosition and ptMaxPosition because Windows fills in their values when the window is minimized or maximized. Clock's SaveWindowState function uses GetWindowPlacement to initialize a WINDOWPLACEMENT structure and then writes the pertinent members of that structure to the registry. The window state is restored when CMyApp::InitInstance calls CMainWindow::RestoreWindowState, which in turn calls GetWindowPlacement to fill in a WINDOWPLACEMENT structure; reads the flags, showCmd, and rcNormalPosition values from the registry; copies them to the structure; and calls SetWindowPlacement. The SW_SHOWMINIMIZED, SW_SHOWMAXIMIZED, or SW_SHOWNORMAL parameter passed to SetWindowPlacement in showCmd makes the window visible, so there's no need to call ShowWindow if RestoreWindowState returns TRUE, indicating that the window state was successfully restored. In fact, you should skip the usual call placed to ShowWindow from InitInstance if RestoreWindowState returns TRUE because the application object's m_nCmdShow parameter might alter the window's state. Clock's InitInstance function looks like this:
BOOL CMyApp::InitInstance ()
{
SetRegistryKey (_T ("Programming Windows with MFC"));
m_pMainWnd = new CMainWindow;
if (!((CMainWindow*) m_pMainWnd)->RestoreWindowState ())
m_pMainWnd->ShowWindow (m_nCmdShow);
m_pMainWnd->UpdateWindow ();
return TRUE;
}
The first time Clock is executed, ShowWindow is called in the normal way because RestoreWindowState returns FALSE. In subsequent invocations, the window's size, position, and visibility state are set by RestoreWindowState, and ShowWindow is skipped.
Before calling SetWindowPlacement to apply the state values retrieved from the registry, RestoreWindowState ensures that a window positioned near the edge of a 1,024-by-768 screen won't disappear if Windows is restarted in 640-by-480 or 800-by-600 mode by comparing the window's normal position with the screen extents:
wp.rcNormalPosition.left = min (wp.rcNormalPosition.left,
::GetSystemMetrics (SM_CXSCREEN) -
::GetSystemMetrics (SM_CXICON));
wp.rcNormalPosition.top = min (wp.rcNormalPosition.top,
::GetSystemMetrics (SM_CYSCREEN) -
::GetSystemMetrics (SM_CYICON));
Called with SM_CXSCREEN and SM_CYSCREEN parameters, ::GetSystemMetrics returns the screen's width and height, respectively, in pixels. If the window coordinates retrieved from the registry are 700 and 600 and Windows is running at a resolution of 640 by 480, this simple procedure transforms the 700 and 600 into 640 and 480 minus the width and height of an icon. Rather than appear out of sight off the screen and probably leave the user wondering why the application didn't start, the window will appear just inside the lower right corner of the screen.
A good way to test a program that preserves a window's position and size is to resize the window to some arbitrary size, maximize it, minimize it, and then close the application with the window minimized. When the program is restarted, the window should come up minimized, and clicking the minimized window's icon in the taskbar should remaximize it. Clicking the restore button should restore the window's original size and position. Try this procedure with Clock, and you should find that it passes the test with flying colors.
Controlling the Window Size: The WM_GETMINMAXINFO Message
A final aspect of Clock that deserves scrutiny is its OnGetMinMaxInfo handler. As a window is being resized, it receives a series of WM_GETMINMAXINFO messages with lParam pointing to a MINMAXINFO structure containing information about the window's minimum and maximum "tracking" sizes. You can limit a window's minimum and maximum sizes programmatically by processing WM_GETMINMAXINFO messages and copying the minimum width and height to the x and y members of the structure's ptMinTrackSize field and the maximum width and height to the x and y members of the ptMaxTrackSize field. Clock prevents its window from being reduced to less than 120 pixels horizontally and vertically with the following OnGetMinMaxInfo handler:
void CMainWindow::OnGetMinMaxInfo (MINMAXINFO* pMMI)
{
pMMI->ptMinTrackSize.x = 120;
pMMI->ptMinTrackSize.y = 120;
}
The tracking dimensions copied to MINMAXINFO are measured in device units, or pixels. In this example, the window's maximum size is unconstrained because pMMI->ptMaxTrackSize is left unchanged. You could limit the maximum window size to one-half the screen size by adding the statements
pMMI->ptMaxTrackSize.x = ::GetSystemMetrics (SM_CXSCREEN) / 2;
pMMI->ptMaxTrackSize.y = ::GetSystemMetrics (SM_CYSCREEN) / 2;
to the message handler.