Getting Input from the Mouse
Windows uses a number of different messages—more than 20 in all—to report input events involving the mouse. These messages fall into two rather broad categories: client-area mouse messages, which report events that occur in a window's client area, and nonclient-area mouse messages, which pertain to events in a window's nonclient area. An "event" can be any of the following:
The press or release of a mouse button
The double click of a mouse button
The movement of the mouse
You'll typically ignore events in the nonclient area of your window and allow Windows to handle them. If your program processes mouse input, it's client-area mouse messages you'll probably be concerned with.
Client-Area Mouse Messages
Windows reports mouse events in a window's client area using the messages shown in the following table.
Client-Area Mouse Messages
Message
Sent When
WM_LBUTTONDOWN
The left mouse button is pressed.
WM_LBUTTONUP
The left mouse button is released.
WM_LBUTTONDBLCLK
The left mouse button is double-clicked.
WM_MBUTTONDOWN
The middle mouse button is pressed.
WM_MBUTTONUP
The middle mouse button is released.
WM_MBUTTONDBLCLK
The middle mouse button is double-clicked.
WM_RBUTTONDOWN
The right mouse button is pressed.
WM_RBUTTONUP
The right mouse button is released.
WM_RBUTTONDBLCLK
The right mouse button is double-clicked.
WM_MOUSEMOVE
The cursor is moved over the window's client area.
Messages that begin with WM_LBUTTON pertain to the left mouse button, WM_MBUTTON messages to the middle mouse button, and WM_RBUTTON messages to the right mouse button. An application won't receive WM_MBUTTON messages if the mouse has only two buttons. (This rule has one important exception: mice that have mouse wheels. Mouse wheels are discussed later in this chapter.) An application won't receive WM_RBUTTON messages if the mouse has just one button. The vast majority of PCs running Windows have two-button mice, so it's reasonably safe to assume that the right mouse button exists. However, if you'd like to be certain (or if you'd like to determine whether there is a third button, too), you can use the Windows ::GetSystemMetrics API function:
int nButtonCount = ::GetSystemMetrics (SM_CMOUSEBUTTONS);
The return value is the number of mouse buttons, or it is 0 in the unlikely event that a mouse is not installed.
WM_xBUTTONDOWN and WM_xBUTTONUP messages report button presses and releases. A WM_LBUTTONDOWN message is normally followed by a WM_LBUTTONUP message, but don't count on that being the case. Mouse messages go to the window under the cursor (the Windows term for the mouse pointer), so if the user clicks the left mouse button over a window's client area and then moves the cursor outside the window before releasing the button, the window receives a WM_LBUTTONDOWN message but not a WM_LBUTTONUP message. Many programs react only to button-down messages and ignore button-up messages, in which case the pairing of the two isn't important. If pairing is essential, a program can "capture" the mouse on receipt of a button-down message and release it when a button-up message arrives. In between, all mouse messages, even those pertaining to events outside the window, are directed to the window that performed the capture. This ensures that a button-up message is received no matter where the cursor is when the button is released. Mouse capturing is discussed later in this chapter.
When two clicks of the same button occur within a very short period of time, the second button-down message is replaced by a WM_xBUTTONDBLCLK message. Significantly, this happens only if the window's WNDCLASS includes the class style CS_DBLCLKS. The default WNDCLASS that MFC registers for frame windows has this style, so frame windows receive double-click messages by default. For a CS_DBLCLKS-style window, two rapid clicks of the left mouse button over the window's client area produce the following sequence of messages:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_LBUTTONUP
If the window is not registered to be notified of double clicks, however, the same two button clicks produce the following sequence of messages:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDOWN
WM_LBUTTONUP
How your application responds to these messages—or whether it responds to them at all—is up to you. You should, however, steer away from having clicks and double clicks of the same mouse button carry out two unrelated tasks. A double-click message is always preceded by a single-click message, so the actions that generate the two messages are not easily divorced. Applications that process single and double clicks of the same button typically select an object on the first click and take some action upon that object on the second click. When you double-click a folder in the right pane of the Windows Explorer, for example, the first click selects the folder and the second click opens it.
WM_MOUSEMOVE messages report that the cursor has moved within the window's client area. As the mouse is moved, the window under the cursor receives a flurry of WM_MOUSEMOVE messages reporting the latest cursor position. Windows has an interesting way of delivering WM_MOUSEMOVE messages that prevents slow applications from being overwhelmed by messages reporting every position in the cursor's path. Rather than stuff a WM_MOUSEMOVE message into the message queue each time the mouse is moved, Windows simply sets a flag in an internal data structure. The next time the application retrieves a message, Windows, seeing that the flag is set, manufactures a WM_MOUSEMOVE message with the current cursor coordinates. Therefore, an application receives WM_MOUSEMOVE messages only as often as it can handle them. If the cursor is moved very slowly, every point in its journey is reported unless the application is busy doing other things. But if the cursor is whisked very rapidly across the screen, most applications receive only a handful of WM_MOUSEMOVE messages.
In an MFC program, message-map entries route mouse messages to class member functions that are provided to handle those messages. The following table lists the message-map macros and message handler names for client-area mouse messages.
Message-Map Macros and Message Handlers for Client-Area Mouse Messages
Message
Message-Map Macro
Handling Function
WM_LBUTTONDOWN
ON_WM_LBUTTONDOWN
OnLButtonDown
WM_LBUTTONUP
ON_WM_LBUTTONUP
OnLButtonUp
WM_LBUTTONDBLCLK
ON_WM_LBUTTONDBLCLK
OnLButtonDblClk
WM_MBUTTONDOWN
ON_WM_MBUTTONDOWN
OnMButtonDown
WM_MBUTTONUP
ON_WM_MBUTTONUP
OnMButtonUp
WM_MBUTTONDBLCLK
ON_WM_MBUTTONDBLCLK
OnMButtonDblClk
WM_RBUTTONDOWN
ON_WM_RBUTTONDOWN
OnRButtonDown
WM_RBUTTONUP
ON_WM_RBUTTONUP
OnRButtonUp
WM_RBUTTONDBLCLK
ON_WM_RBUTTONDBLCLK
OnRButtonDblClk
WM_MOUSEMOVE
ON_WM_MOUSEMOVE
OnMouseMove
OnLButtonDown and other client-area mouse message handlers are prototyped as follows:
afx_msg void OnMsgName (UINT nFlags, CPoint point)
point identifies the location of the cursor. In WM_xBUTTONDOWN and WM_xBUTTONDBLCLK messages, point specifies the location of the cursor when the button was pressed. In WM_xBUTTONUP messages, point specifies the location of the cursor when the button was released. And in WM_MOUSEMOVE messages, point specifies the latest cursor position. In all cases, positions are reported in device coordinates relative to the upper left corner of the window's client area. A WM_LBUTTONDOWN message with point.x equal to 32 and point.y equal to 64 means the left mouse button was clicked 32 pixels to the right of and 64 pixels below the client area's upper left corner. If necessary, these coordinates can be converted to logical coordinates using MFC's CDC::DPtoLP function.
The nFlags parameter specifies the state of the mouse buttons and of the Shift and Ctrl keys at the time the message was generated. You can find out from this parameter whether a particular button or key is up or down by testing for the bit flags listed in the following table.
The nFlags Parameter
Mask
Meaning If Set
MK_LBUTTON
The left mouse button is pressed.
MK_MBUTTON
The middle mouse button is pressed.
MK_RBUTTON
The right mouse button is pressed.
MK_CONTROL
The Ctrl key is pressed.
MK_SHIFT
The Shift key is pressed.
The expression
nFlags & MK_LBUTTON
is nonzero if and only if the left mouse button is pressed, while
nFlags & MK_CONTROL
is nonzero if the Ctrl key was held down when the event occurred. Some programs respond differently to mouse events if the Shift or Ctrl key is held down. For example, a drawing program might constrain the user to drawing only horizontal or vertical lines if the Ctrl key is pressed as the mouse is moved by checking the MK_CONTROL bit in the nFlags parameter accompanying WM_MOUSEMOVE messages. At the conclusion of a drag-and-drop operation, the Windows shell interprets the MK_CONTROL bit to mean that the objects involved in the drop should be copied rather than moved.
The TicTac Application
To show how easy it is to process mouse messages, let's look at a sample application that takes input from the mouse. TicTac, whose output is shown in Figure 3-1, is a tic-tac-toe program that responds to three types of client-area mouse events: left button clicks, right button clicks, and left button double clicks. Clicking the left mouse button over an empty square places an X in that square. Clicking the right mouse button places an O in an empty square. (The program prevents cheating by making sure that Xs and Os are alternated.) Double-clicking the left mouse button over the thick black lines that separate the squares clears the playing grid and starts a new game. After each X or O is placed, the program checks to see if there's a winner or the game has been played to a draw. A draw is declared when all nine squares are filled and neither player has managed to claim three squares in a row horizontally, vertically, or diagonally.
Figure 3-1. The TicTac window.
In addition to providing a hands-on demonstration of mouse-message processing, TicTac also introduces some handy new MFC functions such as CWnd::MessageBox, which displays a message box window, and CRect::PtInRect, which quickly tells you whether a point lies inside a rectangle represented by a CRect object. TicTac's source code appears in Figure 3-2.
Figure 3-2. The TicTac application.
TicTac.h#define EX 1
#define OH 2
class CMyApp : public CWinApp
{
public:
virtual BOOL InitInstance ();
};
class CMainWindow : public CWnd
{
protected:
static const CRect m_rcSquares[9]; // Grid coordinates
int m_nGameGrid[9]; // Grid contents
int m_nNextChar; // Next character (EX or OH)
int GetRectID (CPoint point);
void DrawBoard (CDC* pDC);
void DrawX (CDC* pDC, int nPos);
void DrawO (CDC* pDC, int nPos);
void ResetGame ();
void CheckForGameOver ();
int IsWinner ();
BOOL IsDraw ();
public:
CMainWindow ();
protected:
virtual void PostNcDestroy ();
afx_msg void OnPaint ();
afx_msg void OnLButtonDown (UINT nFlags, CPoint point);
afx_msg void OnLButtonDblClk (UINT nFlags, CPoint point);
afx_msg void OnRButtonDown (UINT nFlags, CPoint point);
DECLARE_MESSAGE_MAP ()
};
TicTac.cpp #include <afxwin.h>
#include "TicTac.h"
CMyApp myApp;
/////////////////////////////////////////////////////////////////////////
// CMyApp member functions
BOOL CMyApp::InitInstance ()
{
m_pMainWnd = new CMainWindow;
m_pMainWnd->ShowWindow (m_nCmdShow);
m_pMainWnd->UpdateWindow ();
return TRUE;
}
/////////////////////////////////////////////////////////////////////////
// CMainWindow message map and member functions
BEGIN_MESSAGE_MAP (CMainWindow, CWnd)
ON_WM_PAINT ()
ON_WM_LBUTTONDOWN ()
ON_WM_LBUTTONDBLCLK ()
ON_WM_RBUTTONDOWN ()
END_MESSAGE_MAP ()
const CRect CMainWindow::m_rcSquares[9] = {
CRect ( 16, 16, 112, 112),
CRect (128, 16, 224, 112),
CRect (240, 16, 336, 112),
CRect ( 16, 128, 112, 224),
CRect (128, 128, 224, 224),
CRect (240, 128, 336, 224),
CRect ( 16, 240, 112, 336),
CRect (128, 240, 224, 336),
CRect (240, 240, 336, 336)
};
CMainWindow::CMainWindow ()
{
m_nNextChar = EX;
::ZeroMemory (m_nGameGrid, 9 * sizeof (int));
//
// Register a WNDCLASS.
//
CString strWndClass = AfxRegisterWndClass (
CS_DBLCLKS, // Class style
AfxGetApp ()->LoadStandardCursor (IDC_ARROW), // Class cursor
(HBRUSH) (COLOR_3DFACE + 1), // Background brush
AfxGetApp ()->LoadStandardIcon (IDI_WINLOGO) // Class icon
);
//
// Create a window.
//
CreateEx (0, strWndClass, _T ("Tic-Tac-Toe"),
WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL);
//
// Size the window.
//
CRect rect (0, 0, 352, 352);
CalcWindowRect (&rect);
SetWindowPos (NULL, 0, 0, rect.Width (), rect.Height (),
SWP_NOZORDER | SWP_NOMOVE | SWP_NOREDRAW);
}
void CMainWindow::PostNcDestroy ()
{
delete this;
}
void CMainWindow::OnPaint ()
{
CPaintDC dc (this);
DrawBoard (&dc);
}
void CMainWindow::OnLButtonDown (UINT nFlags, CPoint point)
{
//
// Do nothing if it's O's turn, if the click occurred outside the
// tic-tac-toe grid, or if a nonempty square was clicked.
//
if (m_nNextChar != EX)
return;
int nPos = GetRectID (point);
if ((nPos == -1) || (m_nGameGrid[nPos] != 0))
return;
//
// Add an X to the game grid and toggle m_nNextChar.
//
m_nGameGrid[nPos] = EX;
m_nNextChar = OH;
//
// Draw an X on the screen and see if either player has won.
//
CClientDC dc (this);
DrawX (&dc, nPos);
CheckForGameOver ();
}
void CMainWindow::OnRButtonDown (UINT nFlags, CPoint point)
{
//
// Do nothing if it's X's turn, if the click occurred outside the
// tic-tac-toe grid, or if a nonempty square was clicked.
//
if (m_nNextChar != OH)
return;
int nPos = GetRectID (point);
if ((nPos == -1) || (m_nGameGrid[nPos] != 0))
return;
//
// Add an O to the game grid and toggle m_nNextChar.
//
m_nGameGrid[nPos] = OH;
m_nNextChar = EX;
//
// Draw an O on the screen and see if either player has won.
//
CClientDC dc (this);
DrawO (&dc, nPos);
CheckForGameOver ();
}
void CMainWindow::OnLButtonDblClk (UINT nFlags, CPoint point)
{
//
// Reset the game if one of the thick black lines defining the game
// grid is double-clicked with the left mouse button.
//
CClientDC dc (this);
if (dc.GetPixel (point) == RGB (0, 0, 0))
ResetGame ();
}
int CMainWindow::GetRectID (CPoint point)
{
//
// Hit-test each of the grid's nine squares and return a rectangle ID
// (0-8) if (point.x, point.y) lies inside a square.
//
for (int i=0; i<9; i++) {
if (m_rcSquares[i].PtInRect (point))
return i;
}
return -1;
}
void CMainWindow::DrawBoard (CDC* pDC)
{
//
// Draw the lines that define the tic-tac-toe grid.
//
CPen pen (PS_SOLID, 16, RGB (0, 0, 0));
CPen* pOldPen = pDC->SelectObject (&pen);
pDC->MoveTo (120, 16);
pDC->LineTo (120, 336);
pDC->MoveTo (232, 16);
pDC->LineTo (232, 336);
pDC->MoveTo (16, 120);
pDC->LineTo (336, 120);
pDC->MoveTo (16, 232);
pDC->LineTo (336, 232);
//
// Draw the Xs and Os.
//
for (int i=0; i<9; i++) {
if (m_nGameGrid[i] == EX)
DrawX (pDC, i);
else if (m_nGameGrid[i] == OH)
DrawO (pDC, i);
}
pDC->SelectObject (pOldPen);
}
void CMainWindow::DrawX (CDC* pDC, int nPos)
{
CPen pen (PS_SOLID, 16, RGB (255, 0, 0));
CPen* pOldPen = pDC->SelectObject (&pen);
CRect rect = m_rcSquares[nPos];
rect.DeflateRect (16, 16);
pDC->MoveTo (rect.left, rect.top);
pDC->LineTo (rect.right, rect.bottom);
pDC->MoveTo (rect.left, rect.bottom);
pDC->LineTo (rect.right, rect.top);
pDC->SelectObject (pOldPen);
}
void CMainWindow::DrawO (CDC* pDC, int nPos)
{
CPen pen (PS_SOLID, 16, RGB (0, 0, 255));
CPen* pOldPen = pDC->SelectObject (&pen);
pDC->SelectStockObject (NULL_BRUSH);
CRect rect = m_rcSquares[nPos];
rect.DeflateRect (16, 16);
pDC->Ellipse (rect);
pDC->SelectObject (pOldPen);
}
void CMainWindow::CheckForGameOver ()
{
int nWinner;
//
// If the grid contains three consecutive Xs or Os, declare a winner
// and start a new game.
//
if (nWinner = IsWinner ()) {
CString string = (nWinner == EX) ?
_T ("X wins!") : _T ("O wins!");
MessageBox (string, _T ("Game Over"), MB_ICONEXCLAMATION | MB_OK);
ResetGame ();
}
//
// If the grid is full, declare a draw and start a new game.
//
else if (IsDraw ()) {
MessageBox (_T ("It's a draw!"), _T ("Game Over"),
MB_ICONEXCLAMATION | MB_OK);
ResetGame ();
}
}
int CMainWindow::IsWinner ()
{
static int nPattern[8][3] = {
0, 1, 2,
3, 4, 5,
6, 7, 8,
0, 3, 6,
1, 4, 7,
2, 5, 8,
0, 4, 8,
2, 4, 6
};
for (int i=0; i<8; i++) {
if ((m_nGameGrid[nPattern[i][0]] == EX) &&
(m_nGameGrid[nPattern[i][1]] == EX) &&
(m_nGameGrid[nPattern[i][2]] == EX))
return EX;
if ((m_nGameGrid[nPattern[i][0]] == OH) &&
(m_nGameGrid[nPattern[i][1]] == OH) &&
(m_nGameGrid[nPattern[i][2]] == OH))
return OH;
}
return 0;
}
BOOL CMainWindow::IsDraw ()
{
for (int i=0; i<9; i++) {
if (m_nGameGrid[i] == 0)
return FALSE;
}
return TRUE;
}
void CMainWindow::ResetGame ()
{
m_nNextChar = EX;
::ZeroMemory (m_nGameGrid, 9 * sizeof (int));
Invalidate ();
}
The first step in processing mouse input is to add entries for the messages you want to handle to the message map. CMainWindow's message map in TicTac.cpp contains the following message-map entries:
ON_WM_LBUTTONDOWN ()
ON_WM_LBUTTONDBLCLK ()
ON_WM_RBUTTONDOWN ()
These three statements correlate WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, and WM_RBUTTONDOWN messages to the CMainWindow member functions OnLButtonDown, OnLButtonDblClk, and OnRButtonDown. When the messages start arriving, the fun begins.
The OnLButtonDown handler processes clicks of the left mouse button in CMainWindow's client area. After checking m_nNextChar to verify that it's X's turn and not O's (and returning without doing anything if it's not), OnLButtonDown calls the protected member function GetRectID to determine whether the click occurred in one of the nine rectangles corresponding to squares in the tic-tac-toe grid. The rectangles' coordinates are stored in the static array of CRect objects named CMainWindow::m_rcSquares. GetRectID uses a for loop to determine whether the cursor location passed to it by the message handler lies inside any of the squares:
for (int i=0; i<9; i++) {
if (m_rcSquares[i].PtInRect (point))
return i;
}
return -1;
CRect::PtInRect returns a nonzero value if the point passed to it lies within the rectangle represented by the CRect object, or 0 if it does not. If PtInRect returns nonzero for any of the rectangles in the m_rcSquares array, GetRectID returns the rectangle ID. The ID is an integer from 0 through 8, with 0 representing the square in the upper left corner of the grid, 1 the square to its right, 2 the square in the upper right corner, 3 the leftmost square in the second row, and so on. Each square has a corresponding element in the m_nGameGrid array, which initially holds all zeros representing empty squares. If none of the calls to PtInRect returns TRUE, GetRectID returns -1 to indicate that the click occurred outside the squares and OnLButtonDown ignores the mouse click. If, however, GetRectID returns a valid ID and the corresponding square is empty, OnLButtonDown records the X in the m_nGameGrid array and calls CMainWindow::DrawX to draw an X in the square. DrawX creates a red pen 16 pixels wide and draws two perpendicular lines oriented at 45-degree angles.
OnRButtonDown works in much the same way as OnLButtonDown, except that it draws an O instead of an X. The routine that does the drawing is CMainWindow::DrawO. Before it draws an O with the CDC::Ellipse function, DrawO selects a NULL brush into the device context:
pDC->SelectStockObject (NULL_BRUSH);
This prevents the interior of the O from being filled with the device context's default white brush. (As an alternative, we could have created a brush whose color matched the window's background color and selected it into the device context. But drawing with a NULL brush is slightly faster because it produces no physical screen output.) The O is then drawn with the statements
CRect rect = m_rcSquares[nPos];
rect.DeflateRect (16, 16);
pDC->Ellipse (rect);
The first statement copies the rectangle representing the grid square to a local CRect object named rect; the second uses CRect::DeflateRect to "deflate" the rectangle by 16 pixels in each direction and form the circle's bounding box; and the third draws the circle. The result is a nicely formed O that's centered in the square in which it is drawn.
Double-clicking the grid lines separating the squares clears the Xs and Os and begins a new game. While this is admittedly a poor way to design a user interface, it does provide an excuse to write a double-click handler. (A better solution would be a push button control with the words New Game stamped on it or a New Game menu item, but since we haven't covered menus and controls yet, the perfect user interface will just have to wait.) Left mouse button double clicks are processed by CMainWindow::OnLButtonDblClk, which contains these simple statements:
CClientDC dc (this);
if (dc.GetPixel (point) == RGB (0, 0, 0))
ResetGame ();
To determine whether the double click occurred over the thick black strokes separating the squares in the playing grid, OnLButtonDblClk calls CDC::GetPixel to get the color of the pixel under the cursor and compares it to black (RGB (0, 0, 0)). If there's a match, ResetGame is called to reset the game. Otherwise, OnLButtonDblClk returns and the double click is ignored. Testing the color of the pixel under the cursor is an effective technique for hit-testing irregularly shaped areas, but be wary of using nonprimary colors that a display driver is likely to dither. Pure black (RGB (0, 0, 0)) and pure white (RGB (255, 255, 255)) are supported on every PC that runs Windows, so you can safely assume that neither of these colors will be dithered.
To be consistent with published user interface guidelines, applications should not use the right mouse button to carry out application-specific tasks as TicTac does. Instead, they should respond to right mouse clicks by popping up context menus. When a WM_RBUTTONUP message is passed to the system for default processing, Windows places a WM_CONTEXTMENU message in the message queue. You'll learn more about this feature of the operating system in the next chapter.
Message Boxes
Before returning, TicTac's OnLButtonDown and OnRButtonDown handlers call CMainWindow::CheckForGameOver to find out if the game has been won or played to a draw. If either player has managed to align three Xs or Os in a row or if no empty squares remain, CheckForGameOver calls CMainWindow's MessageBox function to display a message box announcing the outcome, as shown in Figure 3-3. MessageBox is a function that all window classes inherit from CWnd. It is an extraordinarily useful tool to have at your disposal because it provides a one-step means for displaying a message on the screen and optionally obtaining a response.
Figure 3-3. A Windows message box.
CWnd::MessageBox is prototyped as follows:
int MessageBox (LPCTSTR lpszText, LPCTSTR lpszCaption = NULL,
UINT nType = MB_OK)
lpszText specifies the text in the body of the message box, lpszCaption specifies the caption for the message box's title bar, and nType contains one or more bit flags defining the message box's style. The return value identifies the button that was clicked to dismiss the message box. lpszText and lpszCaption can be CString objects or pointers to conventional text strings. (Because the CString class overloads the LPCTSTR operator, you can always pass a CString to a function that accepts an LPCTSTR data type.) A NULL lpszCaption value displays the caption "Error" in the title bar.
The simplest use for MessageBox is to display a message and pause until the user clicks the message box's OK button:
MessageBox (_T ("Click OK to continue"), _T ("My Application"));
Accepting the default value for nType (MB_OK) means the message box will have an OK button but no other buttons. Consequently, the only possible return value is IDOK. But if you want to use a message box to ask the user whether to save a file before exiting the application, you can use the MB_YESNOCANCEL style:
MessageBox (_T ("Your document contains unsaved data. Save it?"),
_T ("My Application"), MB_YESNOCANCEL);
Now the message box contains three buttons—Yes, No, and Cancel—and the value returned from the MessageBox function is IDYES, IDNO, or IDCANCEL. The program can then test the return value and save the data before closing (IDYES), close without saving (IDNO), or return to the application without shutting down (IDCANCEL). The table below lists the six message box types and the corresponding return values, with the default push button—the one that's "clicked" if the user presses the Enter key—highlighted in boldface type.
Message Box Types
Type
Buttons
Possible Return Codes
MB_ABORTRETRYIGNORE
Abort, Retry, Ignore
IDABORT, IDRETRY, IDIGNORE
MB_OK
OK
IDOK
MB_OKCANCEL
OK, Cancel
IDOK, IDCANCEL
MB_RETRYCANCEL
Retry, Cancel
IDRETRY, IDCANCEL
MB_YESNO
Yes, No
IDYES, IDNO
MB_YESNOCANCEL
Yes, No, Cancel
IDYES, IDNO, IDCANCEL
In message boxes with multiple buttons, the first (leftmost) button is normally the default push button. You can make the second or third button the default by ORing MB_DEFBUTTON2 or MB_DEFBUTTON3 into the value that specifies the message box type. The statement
MessageBox (_T ("Your document contains unsaved data. Save it?"),
_T ("My Application"), MB_YESNOCANCEL ¦ MB_DEFBUTTON3);
displays the same message box as before but makes Cancel the default action.
By default, message boxes are application modal, which means the application that called the MessageBox function is disabled until the message box is dismissed. You can add MB_SYSTEMMODAL to the nType parameter and make the message box system modal. In 16-bit Windows, system-modal means that input to all applications is suspended until the message box is dismissed. In the Win32 environment, Windows makes the message box a topmost window that stays on top of other windows, but the user is still free to switch to other applications. System-modal message boxes should be used only for serious errors that demand immediate attention.
You can add an artistic touch to your message boxes by using MB_ICON identifiers. MB_ICONINFORMATION displays a small text balloon with an "i" for "information" in it in the upper left corner of the message box. The "i" is generally used when information is provided to the user but no questions are being asked, as in
MessageBox (_T ("No errors found. Click OK to continue"),
_T ("My Application"), MB_ICONINFORMATION ¦ MB_OK);
MB_ICONQUESTION displays a question mark instead of an "i" and is normally used for queries such as "Save before closing?" MB_ICONSTOP displays a red circle with an X and usually indicates that an unrecoverable error has occurred—for example, an out-of-memory error is forcing the program to terminate prematurely. Finally, MB_ICONEXCLAMATION displays a yellow triangle containing an exclamation mark. (See Figure 3-3.)
MFC provides an alternative to CWnd::MessageBox in the form of the global AfxMessageBox function. The two are similar, but AfxMessageBox can be called from application classes, document classes, and other non-window classes. One situation in which AfxMessageBox is irreplaceable is when you want to report an error in the application object's InitInstance function. MessageBox requires a valid CWnd pointer and therefore can't be called until after a window is created. AfxMessageBox, on the other hand, can be called at any time.
What? No Frame Window?
TicTac differs from the sample programs in Chapters 1 and 2 in one important respect: Rather than using a frame window for its main window, it derives its own window class from CWnd. It's not that a CFrameWnd wouldn't work; it's that CWnd has everything TicTac needs and more. CWnd is the root of all window classes in MFC. Depending on what kinds of applications you write, deriving from CWnd is something you might need to do often or not at all. Still, it's something every MFC programmer should know how to do, and seeing a window class derived from CWnd also helps to underscore the point that MFC programs don't have to use frame windows.
Creating your own CWnd-derived window class is simple. For starters, you derive the window class from CWnd instead of from CFrameWnd. In the BEGIN_MESSAGE_MAP macro, be sure to specify CWnd, not CFrameWnd, as the base class. Then, in the window's constructor, use AfxRegisterWndClass to register a WNDCLASS and call CWnd::CreateEx to create the window. Remember the beginning of Chapter 1, where we looked at the C source code for an SDK-style Windows application? Before creating a window, WinMain initialized a WNDCLASS structure with values describing the window's class attributes and then called ::RegisterClass to register the WNDCLASS. Normally you don't register a WNDCLASS in an MFC program because MFC registers one for you. Specifying NULL in the first parameter to CFrameWnd::Create accepts the default WNDCLASS. When you derive from CWnd, however, you must register your own WNDCLASS because CWnd::CreateEx does not accept a NULL WNDCLASS name.
The AfxRegisterWndClass Function
MFC makes WNDCLASS registration easy with its global AfxRegisterWndClass function. If you use ::RegisterClass or MFC's AfxRegisterClass to register a WNDCLASS, you must initialize every field in the WNDCLASS structure. But AfxRegisterWndClass fills in most of the fields for you, leaving you to specify values for just the four that MFC applications are typically concerned with. AfxRegisterWndClass is prototyped as follows:
LPCTSTR AfxRegisterWndClass (UINT nClassStyle, HCURSOR hCursor = 0,
HBRUSH hbrBackground = 0, HICON hIcon = 0)
The value returned by AfxRegisterWndClass is a pointer to a null-terminated string containing the WNDCLASS name. Before seeing how TicTac uses AfxRegisterWndClass, let's take a closer look at the function itself and the parameters it accepts.
nClassStyle specifies the class style, which defines certain behavioral characteristics of a window. nClassStyle is a combination of zero or more of the bit flags shown in the following table.
WNDCLASS Style Flags
Class Style
Description
CS_BYTEALIGNCLIENT
Ensures that a window's client area is always aligned on a byte boundary in the video buffer to speed drawing operations.
CS_BYTEALIGNWINDOW
Ensures that the window itself is always aligned on a byte boundary in the video buffer to speed moving and resizing operations.
CS_CLASSDC
Specifies that the window should share a device context with other windows created from the same WNDCLASS.
CS_DBLCLKS
Specifies that the window should be notified of double clicks with WM_xBUTTONDBLCLK messages.
CS_GLOBALCLASS
Registers the WNDCLASS globally so that all applications can use it. (By default, only the application that registers a WNDCLASS can create windows from it.) Used primarily for child window controls.
CS_HREDRAW
Specifies that the entire client area should be invalidated when the window is resized horizontally.
CS_NOCLOSE
Disables the Close command on the system menu and the close button on the title bar.
CS_OWNDC
Specifies that each window created from this WNDCLASS should have its own device context. Helpful when optimizing repaint performance because an application doesn't have to reinitialize a private device context each time the device context is acquired.
CS_PARENTDC
Specifies that a child window should inherit the device context of its parent.
CS_SAVEBITS
Specifies that areas of the screen covered by windows created from this WNDCLASS should be saved in bitmap form for quick repainting. Used primarily for menus and other windows with short life spans.
CS_VREDRAW
Specifies that the entire client area should be invalidated when the window is resized vertically.
The CS_BYTEALIGNCLIENT and CS_BYTEALIGNWINDOW styles were useful back in the days of dumb frame buffers and monochrome video systems, but they are largely obsolete today. CS_CLASSDC, CS_OWNDC, and CS_PARENTDC are used to implement special handling of device contexts. You'll probably use CS_GLOBALCLASS only if you write custom controls to complement list boxes, push buttons, and other built-in control types. The CS_HREDRAW and CS_VREDRAW styles are useful for creating resizeable windows whose content scales with the window size.
hCursor identifies the "class cursor" for windows created from this WNDCLASS. When the cursor moves over a window's client area, Windows retrieves the class cursor's handle from the window's WNDCLASS and uses it to draw the cursor image. You can create custom cursors using an icon editor, or you can use the predefined system cursors that Windows provides. CWinApp::LoadStandardCursor loads a system cursor. The statement
AfxGetApp ()->LoadStandardCursor (IDC_ARROW);
returns the handle of the arrow cursor that most Windows applications use. For a complete list of system cursors, see the documentation for CWinApp::LoadStandardCursor or the ::LoadCursor API function. Generally speaking, only the IDC_ARROW, IDC_IBEAM, and IDC_CROSS cursors are useful as class cursors.
The hbrBackground parameter passed to AfxRegisterWndClass defines the window's default background color. Specifically, hbrBackground identifies the GDI brush that is used to erase the window's interior each time a WM_ERASEBKGND message arrives. A window receives a WM_ERASEBKGND message when it calls ::BeginPaint in response to a WM_PAINT message. If you don't process WM_ERASEBKGND messages yourself, Windows processes them for you by retrieving the class background brush and using it to fill the window's client area. (You can create custom window backgrounds—for example, backgrounds formed from bitmap images—by processing WM_ERASEBKGND messages yourself and returning a nonzero value. The nonzero return prevents Windows from painting the background and overwriting what you wrote.) You can either provide a brush handle for hbrBackground or specify one of the predefined Windows system colors with the value 1 added to it, as in COLOR_WINDOW+1 or COLOR_APPWORKSPACE+1. See the documentation for the ::GetSysColor API function for a complete list of system colors.
The final AfxRegisterWndClass parameter, hIcon, specifies the handle of the icon that Windows uses to represent the application on the desktop, in the taskbar, and elsewhere. You can create a custom icon for your application and load it with CWinApp::LoadIcon, or you can load a predefined system icon with CWinApp::LoadStandardIcon. You can even load icons from other executable files using the ::ExtractIcon API function.
Here's what the code to register a custom WNDCLASS looks like in TicTac.cpp:
CString strWndClass = AfxRegisterWndClass (
CS_DBLCLKS,
AfxGetApp ()->LoadStandardCursor (IDC_ARROW),
(HBRUSH) (COLOR_3DFACE + 1),
AfxGetApp ()->LoadStandardIcon (IDI_WINLOGO)
);
The class style CS_DBLCLKS registers the TicTac window to receive double-click messages. IDC_ARROW tells Windows to display the standard arrow when the cursor is over the TicTac window, and IDI_WINLOGO is one of the standard icons that Windows makes available to all applications. COLOR_3DFACE+1 assigns the TicTac window the same background color as push buttons, dialog boxes, and other 3D display elements. COLOR_3DFACE defaults to light gray, but you can change the color by using the system's Display Properties property sheet. Using COLOR_3DFACE for the background color gives your window the same 3D look as a dialog box or message box and enables it to adapt to changes in the Windows color scheme.
AfxRegisterWndClass and Frame Windows
The AfxRegisterWndClass function isn't only for applications that derive window classes from CWnd; you can also use it to register custom WNDCLASSes for frame windows. The default WNDCLASS that MFC registers for frame windows has the following attributes:
nClassStyle = CS_DBLCLKS ¦ CS_HREDRAW ¦ CS_VREDRAW
hCursor = The handle of the predefined cursor IDC_ARROW
hbrBackground = COLOR_WINDOW+1
hIcon = The handle of the icon whose resource ID is AFX_IDI_STD_FRAME or AFX_IDI_STD_MDIFRAME, or the system icon ID IDI_APPLICATION if no such resource is defined
Suppose you want to create a CFrameWnd frame window that lacks the CS_DBLCLKS style, that uses the IDI_WINLOGO icon, and that uses COLOR_APPWORKSPACE as its default background color. Here's how to create a frame window that meets these qualifications:
CString strWndClass = AfxRegisterWndClass (
CS_HREDRAW ¦ CS_VREDRAW,
AfxGetApp ()->LoadStandardCursor (IDC_ARROW),
(HBRUSH) (COLOR_APPWORKSPACE + 1),
AfxGetApp ()->LoadStandardIcon (IDI_WINLOGO)
);
Create (strWndClass, _T ("My Frame Window"));
These statements replace the
Create (NULL, _T ("My Frame Window"));
statement that normally appears in a frame window's constructor.
More About the TicTac Window
After registering a WNDCLASS, TicTac creates its main window with a call to CWnd::CreateEx:
CreateEx (0, strWndClass, _T ("Tic-Tac-Toe"),
WS_OVERLAPPED ¦ WS_SYSMENU ¦ WS_CAPTION ¦ WS_MINIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL);
The first parameter specifies the extended window style and is a combination of zero or more WS_EX flags. TicTac requires no extended window styles, so this parameter is 0. The second parameter is the WNDCLASS name returned by AfxRegisterWndClass, and the third is the window title. The fourth is the window style. The combination of WS_OVERLAPPED, WS_SYSMENU, WS_CAPTION, and WS_MINIMIZEBOX creates a window that resembles a WS_OVERLAPPEDWINDOW-style window but lacks a maximize button and can't be resized. What is it about the window that makes it nonresizeable? Look up the definition of WS_OVERLAPPEDWINDOW in Winuser.h (one of several large header files that comes with Visual C++), and you'll see something like this:
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED ¦ WS_CAPTION ¦ WS_SYSMENU ¦ WS_THICKFRAME ¦ WS_MINIMIZE ¦ WS_MAXIMIZE)
The WS_THICKFRAME style adds a resizing border whose edges and corners can be grabbed and dragged with the mouse. TicTac's window lacks this style, so the user can't resize it.
The next four parameters passed to CWnd::CreateEx specify the window's initial position and size. TicTac uses CW_USEDEFAULT for all four so that Windows will pick the initial position and size. Yet clearly the TicTac window is not arbitrarily sized; it is sized to match the playing grid. But how? The statements following the call to CreateEx hold the answer:
CRect rect (0, 0, 352, 352);
CalcWindowRect (&rect);
SetWindowPos (NULL, 0, 0, rect.Width (), rect.Height (),
SWP_NOZORDER ¦ SWP_NOMOVE ¦ SWP_NOREDRAW);
The first of these statements creates a CRect object that holds the desired size of the window's client area—352 by 352 pixels. It wouldn't do to pass these values directly to CreateEx because CreateEx's sizing parameters specify the size of the entire window, not just its client area. Since the sizes of the various elements in the window's nonclient area (for example, the height of the title bar) vary with different video drivers and display resolutions, we must calculate the size of the window rectangle from the client rectangle and then size the window to fit.
MFC's CWnd::CalcWindowRect is the perfect tool for the job. Given a pointer to a CRect object containing the coordinates of a window's client area, CalcWindowRect calculates the corresponding window rectangle. The width and height of that rectangle can then be passed to CWnd::SetWindowPos to effect the proper window size. The only catch is that CalcWindowRect must be called after the window is created so that it can factor in the dimensions of the window's nonclient area.
The PostNcDestroy Function
Something you must consider when you derive your own window class from CWnd is that once created, the window object must somehow be deleted. As described in Chapter 2, the last message a window receives before it is destroyed is WM_NCDESTROY. MFC's CWnd class includes a default OnNcDestroy handler that performs some routine cleanup chores and then, as its very last act, calls a virtual function named PostNcDestroy. CFrameWnd objects delete themselves when the windows they are attached to are destroyed; they do this by overriding PostNcDestroy and executing a delete this statement. CWnd::PostNcDestroy does not perform a delete this, so a class derived from CWnd should provide its own version of PostNcDestroy that does. TicTac includes a trivial PostNcDestroy function that destroys the CMainWindow object just before the program terminates:
void CMainWindow::PostNcDestroy ()
{
delete this;
}
The question of "who deletes it" is something you should think about whenever you derive a window class from CWnd. One alternative to overriding CWnd::PostNcDestroy is to override CWinApp::ExitInstance and call delete on the pointer stored in m_pMainWnd.
Nonclient-Area Mouse Messages
When the mouse is clicked inside or moved over a window's nonclient area, Windows sends the window a nonclient-area mouse message. The following table lists the nonclient-area mouse messages.
Nonclient-Area Mouse Messages
Message
Sent When
WM_NCLBUTTONDOWN
The left mouse button is pressed.
WM_NCLBUTTONUP
The left mouse button is released.
WM_NCLBUTTONDBLCLK
The left mouse button is double-clicked.
WM_NCMBUTTONDOWN
The middle mouse button is pressed.
WM_NCMBUTTONUP
The middle mouse button is released.
WM_NCMBUTTONDBLCLK
The middle mouse button is double-clicked.
WM_NCRBUTTONDOWN
The right mouse button is pressed.
WM_NCRBUTTONUP
The right mouse button is released.
WM_NCRBUTTONDBLCLK
The right mouse button is double-clicked.
WM_NCMOUSEMOVE
The cursor is moved over the window's nonclient area.
Notice the parallelism between the client-area mouse messages shown in the table below and the nonclient-area mouse messages; the only difference is the letters NC in the message ID. Unlike double-click messages in a window's client area, WM_NCxBUTTONDBLCLK messages are transmitted regardless of whether the window was registered with the CS_DBLCLKS style.
As with client-area mouse messages, message-map entries route messages to the appropriate class member functions. The following table lists the message-map macros and message handlers for nonclient-area mouse messages.
Message-Map Macros and Message Handlers for Nonclient-Area Mouse Messages
Message
Message-Map Macro
Handling Function
WM_NCLBUTTONDOWN
ON_WM_NCLBUTTONDOWN
OnNcLButtonDown
WM_NCLBUTTONUP
ON_WM_NCLBUTTONUP
OnNcLButtonUp
WM_NCLBUTTONDBLCLK
ON_WM_NCLBUTTONDBLCLK
OnNcLButtonDblClk
WM_NCMBUTTONDOWN
ON_WM_NCMBUTTONDOWN
OnNcMButtonDown
WM_NCMBUTTONUP
ON_WM_NCMBUTTONUP
OnNcMButtonUp
WM_NCMBUTTONDBLCLK
ON_WM_NCMBUTTONDBLCLK
OnNcMButtonDblClk
WM_NCRBUTTONDOWN
ON_WM_NCRBUTTONDOWN
OnNcRButtonDown
WM_NCRBUTTONUP
ON_WM_NCRBUTTONUP
OnNcRButtonUp
WM_NCRBUTTONDBLCLK
ON_WM_NCRBUTTONDBLCLK
OnNcRButtonDblClk
WM_NCMOUSEMOVE
ON_WM_NCMOUSEMOVE
OnNcMouseMove
Message handlers for nonclient-area mouse messages are prototyped this way:
afx_msg void OnMsgName (UINT nHitTest, CPoint point)
Once again, the point parameter specifies the location in the window at which the event occurred. But for nonclient-area mouse messages, point.x and point.y contain screen coordinates rather than client coordinates. In screen coordinates, (0,0) corresponds to the upper left corner of the screen, the positive x and y axes point to the right and down, and one unit in any direction equals one pixel. If you want, you can convert screen coordinates to client coordinates with CWnd::ScreenToClient. The nHitTest parameter contains a hit-test code that identifies where in the window's nonclient area the event occurred. Some of the most interesting hit-test codes are shown in the following table. You'll find a complete list of hit-test codes in the documentation for WM_NCHITTEST or CWnd::OnNcHitTest.
Commonly Used Hit-Test Codes
Value
Corresponding Location
HTCAPTION
The title bar
HTCLOSE
The close button
HTGROWBOX
The restore button (same as HTSIZE)
HTHSCROLL
The window's horizontal scroll bar
HTMENU
The menu bar
HTREDUCE
The minimize button
HTSIZE
The restore button (same as HTGROWBOX)
HTSYSMENU
The system menu box
HTVSCROLL
The window's vertical scroll bar
HTZOOM
The maximize button
Programs don't usually process nonclient-area mouse messages; they allow Windows to process them instead. Windows provides appropriate default responses that frequently result in still more messages being sent to the window. For example, when Windows processes a WM_NCLBUTTONDBLCLK message with a hit-test value equal to HTCAPTION, it sends the window a WM_SYSCOMMAND message with wParam equal to SC_MAXIMIZE or SC_RESTORE to maximize or unmaximize the window. You can prevent double clicks on a title bar from affecting a window by including the following message handler in the window class:
// In CMainWindow's message map
ON_WM_NCLBUTTONDBLCLK ()
void CMainWindow::OnNcLButtonDblClk (UINT nHitTest, CPoint point)
{
if (nHitTest != HTCAPTION)
CWnd::OnNcLButtonDblClk (nHitTest, point);
}
Calling the base class's OnNcLButtonDblClk handler passes the message to Windows and allows default processing to take place. Returning without calling the base class prevents Windows from knowing that the double click occurred. You can use other hit-test values to customize the window's response to other nonclient-area mouse events.