Programming Windows with MFC, Second Edition
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 () |
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.
The WM_NCHITTEST Message
Before a window receives a client-area or nonclient-area mouse message, it receives a WM_NCHITTEST message accompanied by the cursor's screen coordinates. Most applications don't process WM_NCHITTEST messages, instead electing to let Windows process them. When Windows processes a WM_NCHITTEST message, it uses the cursor coordinates to determine what part of the window the cursor is over and then generates either a client-area or nonclient-area mouse message.
One clever use of an OnNcHitTest handler is for substituting the HTCAPTION hit-test code for HTCLIENT, which creates a window that can be dragged by its client area:
// In CMainWindow's message map ON_WM_NCHITTEST () |
As this example demonstrates, WM_NCHITTEST messages that you don't process yourself should be forwarded to the base class so that other aspects of the program's operation aren't affected.
The WM_MOUSELEAVE and WM_MOUSEHOVER Messages
It's easy to tell when the cursor enters a window or moves over it because the window receives WM_MOUSEMOVE messages. The ::TrackMouseEvent function, which debuted in Windows NT 4.0 and is also supported in Windows 98, makes it equally easy to determine when the cursor leaves a window or hovers motionlessly over the top of it. With ::TrackMouseEvent, an application can register to receive WM_MOUSELEAVE messages when the cursor leaves a window and WM_MOUSEHOVER messages when the cursor hovers over a window.
::TrackMouseEvent accepts just one parameter: a pointer to a TRACKMOUSEEVENT structure. The structure is defined this way in Winuser.h:
typedef struct tagTRACKMOUSEEVENT { DWORD cbSize; DWORD dwFlags; HWND hwndTrack; DWORD dwHoverTime; } TRACKMOUSEEVENT; |
cbSize holds the size of the structure. dwFlags holds bit flags specifying what the caller wants to do: register to receive WM_MOUSELEAVE messages (TME_LEAVE), register to receive WM_MOUSEHOVER messages (TME_HOVER), cancel WM_MOUSELEAVE and WM_MOUSEHOVER messages (TME_CANCEL), or have the system fill the TRACKMOUSEEVENT structure with the current ::TrackMouseEvent settings (TME_QUERY). hwndTrack is the handle of the window for which WM_MOUSELEAVE and WM_MOUSEHOVER messages are generated. dwHoverTime is the length of time in milliseconds that the cursor must pause before a WM_MOUSEHOVER message is sent to the underlying window. You can accept the system default of 400 milliseconds by setting dwHoverTime equal to HOVER_DEFAULT.
The cursor doesn't have to be perfectly still for the system to generate a WM_MOUSEHOVER message. If the cursor stays within a rectangle whose width and height equal the values returned by ::SystemParametersInfo when it's called with SPI_GETMOUSEHOVERWIDTH and SPI_GETMOUSEHOVERHEIGHT values, and if it stays there for the number of milliseconds returned by ::SystemParametersInfo when it's called with an SPI_GETMOUSEHOVERTIME value, a WM_MOUSEHOVER message ensues. If you want, you can change these parameters by calling ::SystemParametersInfo with SPI_SETMOUSEHOVERWIDTH, SPI_SETMOUSEHOVERHEIGHT, and SPI_SETMOUSEHOVERTIME values.
One of the more interesting aspects of ::TrackMouseEvent is that its effects are cancelled when a WM_MOUSELEAVE or WM_MOUSEHOVER message is generated. This means that if you want to receive these message anytime the cursor exits or pauses over a window, you must call ::TrackMouseEvent again whenever a WM_MOUSELEAVE or WM_MOUSEHOVER message is received. To illustrate, the following code snippet writes "Mouse enter," "Mouse leave," or "Mouse hover" to the debug output window anytime the mouse enters, leaves, or pauses over a window. m_bMouseOver is a BOOL CMainWindow member variable. It should be set to FALSE in the class constructor:
// In the message map ON_WM_MOUSEMOVE () ON_MESSAGE (WM_MOUSELEAVE, OnMouseLeave) ON_MESSAGE (WM_MOUSEHOVER, OnMouseHover) |
MFC doesn't provide type-specific message-mapping macros for WM_MOUSELEAVE and WM_MOUSEHOVER messages, so as this example demonstrates, you must use the ON_MESSAGE macro to link these messages to class member functions. The lParam value accompanying a WM_MOUSEHOVER message holds the cursor's x coordinate in its low word and the cursor's y coordinate in its high word. wParam is unused. Both wParam and lParam are unused in WM_MOUSELEAVE messages.
One final note regarding ::TrackMouseEvent: In order to use it, you must include the following #define in your source code:
#define _WIN32_WINNT 0x0400 |
Be sure to include this line before the line that #includes Afxwin.h. Otherwise, it will have no effect.
The Mouse Wheel
Many of the mice used with Windows today include a wheel that can be used to scroll a window without clicking the scroll bar. When the wheel is rolled, the window with the input focus receives WM_MOUSEWHEEL messages. MFC's CScrollView class provides a default handler for these messages that automatically scrolls the window, but if you want mouse wheel messages to scroll a non-CScrollView window, you must process WM_MOUSEWHEEL messages yourself.
MFC's ON_WM_MOUSEWHEEL macro maps WM_MOUSEWHEEL messages to the message handler OnMouseWheel. OnMouseWheel is prototyped like this:
BOOL OnMouseWheel (UINT nFlags, short zDelta, CPoint point) |
The nFlags and point parameters are identical to those passed to OnLButtonDown. zDelta is the distance the wheel was rotated. A zDelta equal to WHEEL_DELTA (120) means the wheel was rotated forward one increment, or notch, and _WHEEL_DELTA means the wheel was rotated backward one notch. If the wheel is rotated forward five notches, the window will receive five WM_MOUSEWHEEL messages, each with a zDelta of WHEEL_DELTA. OnMouseWheel should return a nonzero value if it scrolled the window, or zero if it did not.
A simple way to respond to a WM_MOUSEWHEEL message is to scroll the window one line up (if zDelta is positive) or one line down (if zDelta is negative) for every WHEEL_DELTA unit. The recommended approach, however, is slightly more involved. First you ask the system for the number of lines that corresponds to WHEEL_DELTA units. In Windows NT 4.0 and higher and in Windows 98, you can get this value by calling ::SystemParametersInfo with a first parameter equal to SPI_GETWHEELSCROLLLINES. Then you multiply the result by zDelta and divide by WHEEL_DELTA to determine how many lines to scroll. You can modify the Accel program presented in Chapter 2 to respond to WM_MOUSEWHEEL messages in this manner by adding the following message-map entry and message handler to CMainWindow:
// In the message map ON_WM_MOUSEWHEEL () |
Dividing zDelta by WHEEL_DELTA ensures that the application won't scroll too quickly if, in the future, it's used with a mouse that has a wheel granularity less than 120 units. WHEEL_PAGESCROLL is a special value that indicates the application should simulate a click of the scroll bar shaft—in other words, perform a page-up or page-down. Both WHEEL_DELTA and WHEEL_PAGESCROLL are defined in Winuser.h.
One issue to be aware of regarding this code sample is that it's not compatible with Windows 95. Why? Because calling ::SystemParametersInfo with an SPI_GETWHEELSCROLLLINES value does nothing in Windows 95. If you want to support Windows 95, you can either assume that ::SystemParametersInfo would return 3 (the default) or resort to more elaborate means to obtain the user's preference. MFC uses an internal function named _AfxGetMouseScrollLines to get this value. _AfxGetMouseScrollLines is platform-neutral; it uses various methods to attempt to obtain a scroll line count and defaults to 3 if none of those methods work. See the MFC source code file Viewscrl.cpp if you'd like to mimic that behavior in your code.
If the mouse wheel is clicked rather than rotated, the window under the cursor generally receives middle-button mouse messages—WM_MBUTTONDOWN messages when the wheel is pressed, WM_MBUTTONUP messages when the wheel is released. (I say "generally" because this is the default behavior; it can be changed through the Control Panel.) Some applications respond to wheel clicks in a special way. Microsoft Word 97, for example, scrolls the currently displayed document when it receives WM_MOUSEMOVE messages with the wheel held down. Knowing that the mouse wheel produces middle-button messages, you can customize your applications to respond to mouse wheel events any way you see fit.
Capturing the Mouse
One problem that frequently crops up in programs that process mouse messages is that the receipt of a button-down message doesn't necessarily mean that a button-up message will follow. Suppose you've written a drawing program that saves the point parameter passed to OnLButtonDown and uses it as an anchor point to draw a line whose other endpoint follows the cursor—an action known as "rubber-banding" a line. When a WM_LBUTTONUP message arrives, the application erases the rubber-band line and draws a real line in its place. But what happens if the user moves the mouse outside the window's client area before releasing the mouse button? The application never gets that WM_LBUTTONUP message, so the rubber-band line is left hanging in limbo and the real line isn't drawn.
Windows provides an elegant solution to this problem by allowing an application to "capture" the mouse upon receiving a button-down message and to continue receiving mouse messages no matter where the cursor goes on the screen until the button is released or the capture is canceled. (In the Win32 environment, to prevent applications from monopolizing the mouse, the system stops sending mouse messages to a window that owns the capture if the button is released.) The mouse is captured with CWnd::SetCapture and released with ::ReleaseCapture. Calls to these functions are normally paired in button-down and button-up handlers, as shown here:
// In CMainWindow's message map ON_WM_LBUTTONDOWN () ON_WM_LBUTTONUP () |
In between, CMainWindow receives WM_MOUSEMOVE messages that report the cursor position even if the cursor leaves it. Client-area mouse messages continue to report cursor positions in client coordinates, but coordinates can now go negative and can also exceed the dimensions of the window's client area.
A related function, CWnd::GetCapture, returns a CWnd pointer to the window that owns the capture. In the Win32 environment, GetCapture returns NULL if the mouse is not captured or if it's captured by a window belonging to another thread. The most common use of GetCapture is for determining whether your own window has captured the mouse. The statement
if (GetCapture () == this) |
is true if and only if the window identified by this currently has the mouse captured.
How does capturing the mouse solve the problem with the rubber-banded line? By capturing the mouse in response to a WM_LBUTTONDOWN message and releasing it when a WM_LBUTTONUP message arrives, you're guaranteed to get the WM_LBUTTONUP message when the mouse button is released. The sample program in the next section illustrates the practical effect of this technique.
Mouse Capturing in Action
The MouseCap application shown in Figure 3-4 is a rudimentary paint program that lets the user draw lines with the mouse. To draw a line, press the left mouse button anywhere in the window's client area and drag the cursor with the button held down. As the mouse is moved, a thin line is rubber-banded between the anchor point and the cursor. When the mouse button is released, the rubber-band line is erased and a red line 16 pixels wide is drawn in its place. Because the mouse is captured while the button is depressed, rubber-banding works even if the mouse is moved outside the window. And no matter where the cursor is when the mouse button is released, a red line is drawn between the anchor point and the endpoint. MouseCap's source code appears in Figure 3-5.
Figure 3-4. The MouseCap window.
Figure 3-5. The MouseCap application.
MouseCap.h
class CMyApp : public CWinApp { public: virtual BOOL InitInstance (); }; class CMainWindow : public CFrameWnd { protected: BOOL m_bTracking; // TRUE if rubber banding BOOL m_bCaptureEnabled; // TRUE if capture enabled CPoint m_ptFrom; // "From" point for rubber banding CPoint m_ptTo; // "To" point for rubber banding void InvertLine (CDC* pDC, CPoint ptFrom, CPoint ptTo); public: CMainWindow (); protected: afx_msg void OnLButtonDown (UINT nFlags, CPoint point); afx_msg void OnLButtonUp (UINT nFlags, CPoint point); afx_msg void OnMouseMove (UINT nFlags, CPoint point); afx_msg void OnNcLButtonDown (UINT nHitTest, CPoint point); DECLARE_MESSAGE_MAP () }; |
MouseCap.cpp
#include <afxwin.h> #include "MouseCap.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, CFrameWnd) ON_WM_LBUTTONDOWN () ON_WM_LBUTTONUP () ON_WM_MOUSEMOVE () ON_WM_NCLBUTTONDOWN () END_MESSAGE_MAP () CMainWindow::CMainWindow () { m_bTracking = FALSE; m_bCaptureEnabled = TRUE; // // Register a WNDCLASS. // CString strWndClass = AfxRegisterWndClass ( 0, AfxGetApp ()->LoadStandardCursor (IDC_CROSS), (HBRUSH) (COLOR_WINDOW + 1), AfxGetApp ()->LoadStandardIcon (IDI_WINLOGO) ); // // Create a window. // Create (strWndClass, _T ("Mouse Capture Demo (Capture Enabled)")); } void CMainWindow::OnLButtonDown (UINT nFlags, CPoint point) { // // Record the anchor point and set the tracking flag. // m_ptFrom = point; m_ptTo = point; m_bTracking = TRUE; // // If capture is enabled, capture the mouse. // if (m_bCaptureEnabled) SetCapture (); } void CMainWindow::OnMouseMove (UINT nFlags, CPoint point) { // // If the mouse is moved while we're "tracking" (that is, while a // line is being rubber-banded), erase the old rubber-band line and // draw a new one. // if (m_bTracking) { CClientDC dc (this); InvertLine (&dc, m_ptFrom, m_ptTo); InvertLine (&dc, m_ptFrom, point); m_ptTo = point; } } void CMainWindow::OnLButtonUp (UINT nFlags, CPoint point) { // // If the left mouse button is released while we're tracking, release // the mouse if it's currently captured, erase the last rubber-band // line, and draw a thick red line in its place. // if (m_bTracking) { m_bTracking = FALSE; if (GetCapture () == this) ::ReleaseCapture (); CClientDC dc (this); InvertLine (&dc, m_ptFrom, m_ptTo); CPen pen (PS_SOLID, 16, RGB (255, 0, 0)); dc.SelectObject (&pen); dc.MoveTo (m_ptFrom); dc.LineTo (point); } } void CMainWindow::OnNcLButtonDown (UINT nHitTest, CPoint point) { // // When the window's title bar is clicked with the left mouse button, // toggle the capture flag on or off and update the window title. // if (nHitTest == HTCAPTION) { m_bCaptureEnabled = m_bCaptureEnabled ? FALSE : TRUE; SetWindowText (m_bCaptureEnabled ? _T ("Mouse Capture Demo (Capture Enabled)") : _T ("Mouse Capture Demo (Capture Disabled)")); } CFrameWnd::OnNcLButtonDown (nHitTest, point); } void CMainWindow::InvertLine (CDC* pDC, CPoint ptFrom, CPoint ptTo) { // //Invert a line of pixels by drawing a line in the R2_NOT drawing mode. // int nOldMode = pDC->SetROP2 (R2_NOT); pDC->MoveTo (ptFrom); pDC->LineTo (ptTo); pDC->SetROP2 (nOldMode); } |
Most of the action takes place in the program's OnLButtonDown, OnMouseMove, and OnLButtonUp handlers. OnLButtonDown starts the drawing process by initializing a trio of variables that are members of the CMainWindow class:
m_ptFrom = point; m_ptTo = point; m_bTracking = TRUE; |
m_ptFrom and m_ptTo are the starting and ending points for the rubber-band line. m_ptTo is continually updated by the OnMouseMove handler as the mouse is moved. m_bTracking, which is TRUE when the left button is down and FALSE when it is not, is a flag that tells OnMouseMove and OnLButtonUp whether a line is being rubber-banded. OnLButtonDown's only other action is to capture the mouse if m_bCaptureEnabled is TRUE:
if (m_bCaptureEnabled) SetCapture (); |
m_bCaptureEnabled is initialized to TRUE by CMainWindow's constructor. It is toggled by the window's OnNcLButtonDown handler so that you can turn mouse capturing on and off and see the effect that mouse capturing has on the program's operation. (More on this in a moment.)
OnMouseMove's job is to move the rubber-band line and update m_ptTo with the new cursor position whenever the mouse is moved. The statement
InvertLine (&dc, m_ptFrom, m_ptTo); |
erases the previously drawn rubber-band line, and
InvertLine (&dc, m_ptFrom, point); |
draws a new one. InvertLine is a member of CMainWindow. It draws a line not by setting each pixel to a certain color, but by inverting the existing pixel colors. This ensures that the line can be seen no matter what background it is drawn against and that drawing the line again in the same location will erase it by restoring the original screen colors. The inversion is accomplished by setting the device context's drawing mode to R2_NOT with the statement
int nOldMode = pDC->SetROP2 (R2_NOT); |
See Chapter 2 for a discussion of R2_NOT and other drawing modes.
When the left mouse button is released, CMainWindow::OnLButtonUp is called. After setting m_bTracking to FALSE and releasing the mouse, it erases the rubber-band line drawn by the last call to OnMouseMove:
CClientDC dc (this); InvertLine (&dc, m_ptFrom, m_ptTo); |
OnLButtonUp then creates a solid red pen 16 pixels wide, selects it into the device context, and draws a thick red line:
CPen pen (PS_SOLID, 16, RGB (255, 0, 0)); dc.SelectObject (&pen); dc.MoveTo (m_ptFrom); dc.LineTo (point); |
Its work done, OnLButtonUp returns, and the drawing operation is complete. Figure 3-4 above shows what the MouseCap window looks like after a few lines have been drawn and as a new line is rubber-banded.
After you've played around with the program a bit, click the title bar to activate the OnNcLButtonDown handler and toggle the m_bCaptureEnabled flag from TRUE to FALSE. The window title should change from "Mouse Capture Demo (Capture Enabled)" to "Mouse Capture Demo (Capture Disabled)." OnNcLButtonDown processes left button clicks in the nonclient area and uses CWnd::SetWindowText to change the window title if the hit-test code in nHitTest is equal to HTCAPTION, indicating that the click occurred in the title bar.
Now draw a few lines with mouse capturing disabled. Observe that if you move the mouse outside the window while rubber-banding, the line freezes until the mouse reenters the client area, and that if you release the mouse button outside the window, the program gets out of sync. The rubber-band line follows the mouse when you move it back to the interior of the window (even though the mouse button is no longer pressed), and it never gets erased. Click the title bar once again to reenable mouse capturing, and the program will revert to its normal self.
The Cursor
Rather than use the arrow-shaped cursor you see in most Windows applications, MouseCap uses a crosshair cursor. Arrows and crosshairs are just two of several predefined cursor types that Windows places at your disposal, and if none of the predefined cursors fits the bill, you can always create your own. As usual, Windows gives programmers a great deal of latitude in this area.
First, a bit of background on how cursors work. As you know, every window has a corresponding WNDCLASS whose characteristics are defined in a WNDCLASS structure. One of the fields of the WNDCLASS structure is hCursor, which holds the handle of the class cursor—the image displayed when the cursor is over a window's client area. When the mouse is moved, Windows erases the cursor from its old location by redrawing the background behind it. Then it sends the window under the cursor a WM_SETCURSOR message containing a hit-test code. The system's default response to this message is to call ::SetCursor to display the class cursor if the hit-test code is HTCLIENT or to display an arrow if the hit-test code indicates that the cursor is outside the client area. As a result, the cursor is automatically updated as it is moved about the screen. When you move the cursor into an edit control, for example, it changes into a vertical bar or "I-beam" cursor. This happens because Windows registers a special WNDCLASS for edit controls and specifies the I-beam cursor as the class cursor.
It follows that one way to change the cursor's appearance is to register a WNDCLASS and specify the desired cursor type as the class cursor. In MouseCap, CMainWindow's constructor registers a WNDCLASS whose class cursor is IDC_CROSS and passes the WNDCLASS name to CFrameWnd::Create:
CString strWndClass = AfxRegisterWndClass ( 0, AfxGetApp ()->LoadStandardCursor (IDC_CROSS), (HBRUSH) (COLOR_WINDOW + 1), AfxGetApp ()->LoadStandardIcon (IDI_WINLOGO) ); Create (strWndClass, _T ("Mouse Capture Demo (Capture Enabled)")); |
Windows then displays a crosshair cursor anytime the mouse pointer is positioned in CMainWindow's client area.
A second way to customize the cursor is to call the API function ::SetCursor in response to WM_SETCURSOR messages. The following OnSetCursor function displays the cursor whose handle is stored in CMainWindow::m_hCursor when the cursor is over CMainWindow's client area:
// In CMainWindow's message map ON_WM_SETCURSOR () |
Returning TRUE after calling ::SetCursor tells Windows that the cursor has been set. WM_SETCURSOR messages generated outside the window's client area are passed to the base class so that the default cursor is displayed. The class cursor is ignored because OnSetCursor never gives Windows the opportunity to display it.
Why would you want to use OnSetCursor rather than just registering m_hCursor as the class cursor? Suppose you want to display an arrow cursor when the cursor is in the top half of the window and an I-beam cursor when the cursor is in the bottom half. A class cursor won't suffice in this case, but OnSetCursor will do the job quite nicely. The following OnSetCursor handler sets the cursor to either m_hCursorArrow or m_hCursorIBeam when the cursor is in CMainWindow's client area:
BOOL CMainWindow::OnSetCursor (CWnd* pWnd, UINT nHitTest, UINT message) { if (nHitTest == HTCLIENT) { DWORD dwPos = ::GetMessagePos (); CPoint point (LOWORD (dwPos), HIWORD (dwPos)); ScreenToClient (&point); CRect rect; GetClientRect (&rect); ::SetCursor ((point.y < rect.Height () / 2) ? m_hCursorArrow : m_hCursorIBeam); return TRUE; } return CFrameWnd::OnSetCursor (pWnd, nHitTest, message); } |
::GetMessagePos returns a DWORD value containing the cursor's x and y screen coordinates at the moment the WM_SETCURSOR message was retrieved from the message queue. CWnd::ScreenToClient converts screen coordinates to client coordinates. If the converted point's y coordinate is less than half the height of the window's client area, the cursor is set to m_hCursorArrow. But if y is greater than or equal to half the client area height, the cursor is set to m_hCursorIBeam instead. The VisualKB application presented later in this chapter uses a similar technique to change the cursor to an I-beam when it enters a rectangle surrounding a text-entry field.
Should the need ever arise, you can hide the cursor with the statement
::ShowCursor (FALSE); |
and display it again with
::ShowCursor (TRUE); |
Internally, Windows maintains a display count that's incremented each time ::ShowCursor (TRUE) is called and decremented by each call to ::ShowCursor (FALSE). The count is initially set to 0 if a mouse is installed and to -1 if no mouse is present, and the cursor is displayed whenever the count is greater than or equal to 0. Thus, if you call ::ShowCursor (FALSE) twice to hide the cursor, you must call ::ShowCursor (TRUE) twice to display it again.
The Hourglass Cursor
When an application responds to a message by undertaking a lengthy processing task, it's customary to change the cursor to an hourglass to remind the user that the application is "busy." (While a message handler executes, no further messages are retrieved from the message queue and the program is frozen to input. In Chapter 17, you'll learn about ways to perform background processing tasks while continuing to retrieve and dispatch messages.)
Windows provides the hourglass cursor for you; its identifier is IDC_WAIT. An easy way to display an hourglass cursor is to declare a CWaitCursor variable on the stack, like this:
CWaitCursor wait; |
CWaitCursor's constructor displays an hourglass cursor, and its destructor restores the original cursor. If you'd like to restore the cursor before the variable goes out of scope, simply call CWaitCursor::Restore:
wait.Restore (); |
You should call Restore before taking any action that would allow a WM_SETCURSOR message to seep through and destroy the hourglass—for example, before displaying a message box or a dialog box.
You can change the cursor displayed by CWaitCursor::CWaitCursor and BeginWaitCursor by overriding CWinApp's virtual DoWaitCursor function. Use the default implementation of CWinApp::DoWaitCursor found in the MFC source code file Appui.cpp as a model for your own implementations.
Mouse Miscellanea
As mentioned earlier, calling the ::GetSystemMetrics API function with an SM_CMOUSEBUTTONS argument queries the system for the number of mouse buttons. (There is no MFC equivalent to ::GetSystemMetrics, so you must call it directly.) The usual return value is 1, 2, or 3, but a 0 return means no mouse is attached. You can also find out whether a mouse is present by calling ::GetSystemMetrics this way:
::GetSystemMetrics (SM_MOUSEPRESENT) |
The return value is nonzero if there is a mouse attached, 0 if there is not. In the early days of Windows, programmers had to consider the possibility that someone might be using Windows without a mouse. Today that's rarely a concern, and a program that queries the system to determine whether a mouse is present is a rare program indeed.
Other mouse-related ::GetSystemMetrics parameters include SM_CXDOUBLECLK and SM_CYDOUBLECLK, which specify the maximum horizontal and vertical distances (in pixels) that can separate the two halves of a double click, and SM_SWAPBUTTON, which returns a nonzero value if the user has swapped the left and right mouse buttons using the Control Panel. When the mouse buttons are swapped, the left mouse button generates WM_RBUTTON messages and the right mouse button generates WM_LBUTTON messages. Generally you don't need to be concerned about this, but if for some reason your application wants to be sure that the left mouse button really means the left mouse button, it can use ::GetSystemMetrics to determine whether the buttons have been swapped.
The API functions ::SetDoubleClickTime and ::GetDoubleClickTime enable an application to set and retrieve the mouse double-click time—the maximum amount of time permitted between clicks when a mouse button is double-clicked. The expression
::GetDoubleClickTime () |
returns the double-click time in milliseconds, while the statement
::SetDoubleClickTime (250); |
sets the double-click time to 250 milliseconds, or one quarter of a second. When the same mouse button is clicked twice in succession, Windows uses both the double-click time and the SM_CXDOUBLECLK and SM_CYDOUBLECLK values returned by ::GetSystemMetrics to determine whether to report the second of the two clicks as a double click.
A function that processes mouse messages can determine which, if any, mouse buttons are pressed by checking the nFlags parameter passed to the message handler. It's also possible to query the state of a mouse button outside a mouse message handler by calling ::GetKeyState or ::GetAsyncKeyState with a VK_LBUTTON, VK_MBUTTON, or VK_RBUTTON parameter. ::GetKeyState should be called only from a keyboard message handler because it returns the state of the specified mouse button at the time the keyboard message was generated. ::GetAsyncKeyState can be called anywhere, anytime. It works in real time, returning the state of the button at the moment the function is called. A negative return value from
::GetKeyState (VK_LBUTTON) |
or
::GetAsyncKeyState (VK_LBUTTON) |
indicates that the left mouse button is pressed. Swapping the mouse buttons does not affect ::GetAsyncKeyState, so if you use this function, you should also use ::GetSystemMetrics to determine whether the buttons have been swapped. The expression
::GetAsyncKeyState (::GetSystemMetrics (SM_SWAPBUTTON) ? VK_RBUTTON : VK_LBUTTON) |
checks the state of the left mouse button asynchronously and automatically queries the right mouse button instead if the buttons have been swapped.
Windows provides a pair of API functions named ::GetCursorPos and ::SetCursorPos for getting and setting the cursor position manually. ::GetCursorPos copies the cursor coordinates to a POINT structure. A related function named ::GetMessagePos returns a DWORD value containing a pair of 16-bit coordinates specifying where the cursor was when the last message was retrieved from the message queue. You can extract those coordinates using the Windows LOWORD and HIWORD macros:
DWORD dwPos = ::GetMessagePos (); int x = LOWORD (dwPos); int y = HIWORD (dwPos); |
::GetCursorPos and ::GetMessagePos both report the cursor position in screen coordinates. Screen coordinates can be converted to client coordinates by calling a window's ClientToScreen function.
Windows also provides a function named ::ClipCursor that restricts the cursor to a particular area of the screen. ::ClipCursor accepts a pointer to a RECT structure that describes, in screen coordinates, the clipping rectangle. Since the cursor is a global resource shared by all applications, an application that uses ::ClipCursor must free the cursor by calling
::ClipCursor (NULL); |
before terminating, or else the cursor will remain locked into the clipping rectangle indefinitely.