VC++ 6.0 ebook chapter index
homepage aranna.altervista.org
Free counters!

Client-Area Mouse Messages

In the previous chapter, you saw how Windows sends keyboard messages only to the window that has the input focus. Mouse messages are different: a window procedure receives mouse messages whenever the mouse passes over the window or is clicked within the window, even if the window is not active or does not have the input focus. Windows defines 21 messages for the mouse. However, 11 of these messages do not relate to the client area. These are called "nonclient-area messages," and Windows applications usually ignore them.

When the mouse is moved over the client area of a window, the window procedure receives the message WM_MOUSEMOVE. When a mouse button is pressed or released within the client area of a window, the window procedure receives the messages in this table:
Button Pressed Released Pressed (Second Click)
Left WM_LBUTTONDOWN WM_LBUTTONUP WM_LBUTTONDBLCLK
Middle WM_MBUTTONDOWN WM_MBUTTONUP WM_MBUTTONDBLCLK
Right WM_RBUTTONDOWN WM_RBUTTONUP WM_RBUTTONDBLCLK

Your window procedure receives MBUTTON messages only for a three-button mouse and RBUTTON messages only for a two-button mouse. The window procedure receives DBLCLK (double-click) messages only if the window class has been defined to receive them (as described in the section titled "Mouse Double-Clicks").

For all these messages, the value of lParam contains the position of the mouse. The low word is the x-coordinate, and the high word is the y-coordinate relative to the upper left corner of the client area of the window. You can extract these values using the LOWORD and HIWORD macros:

x = LOWORD (lParam) ;
y = HIWORD (lParam) ;

The value of wParam indicates the state of the mouse buttons and the Shift and Ctrl keys. You can test wParam using these bit masks defined in the WINUSER.H header file. The MK prefix stands for "mouse key."

MK_LBUTTON     Left button is down
MK_MBUTTON     Middle button is down
MK_RBUTTON     Right button is down
MK_SHIFT       Shift key is down
MK_CONTROL     Ctrl key is down

For example, if you receive a WM_LBUTTONDOWN message, and if the value

wparam & MK_SHIFT

is TRUE (nonzero), you know that the Shift key was down when the left button was pressed.

As you move the mouse over the client area of a window, Windows does not generate a WM_MOUSEMOVE message for every possible pixel position of the mouse. The number of WM_MOUSEMOVE messages your program receives depends on the mouse hardware and on the speed at which your window procedure can process the mouse movement messages. In other words, Windows does not fill up a message queue with unprocessed WM_MOUSEMOVE messages. You'll get a good idea of the rate of WM_MOUSEMOVE messages when you experiment with the CONNECT program described below.

If you click the left mouse button in the client area of an inactive window, Windows changes the active window to the window that is being clicked and then passes the WM_LBUTTONDOWN message to the window procedure. When your window procedure gets a WM_LBUTTONDOWN message, your program can safely assume the window is active. However, your window procedure can receive a WM_LBUTTONUP message without first receiving a WM_LBUTTONDOWN message. This can happen if the mouse button is pressed in one window, moved to your window, and released. Similarly, the window procedure can receive a WM_LBUTTONDOWN without a corresponding WM_LBUTTONUP message if the mouse button is released while positioned over another window.

There are two exceptions to these rules:

Simple Mouse Processing: An Example

The CONNECT program, shown in Figure 7-1, does some simple mouse processing to let you get a good feel for how Windows sends mouse messages to your program.

Figure 7-1. The CONNECT program.

CONNECT.C

/*--------------------------------------------------
   CONNECT.C -- Connect-the-Dots Mouse Demo Program
                (c) Charles Petzold, 1998
  --------------------------------------------------*/

#include <windows.h>

#define MAXPOINTS 1000

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("Connect") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;
     
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;
     
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("Program requires Windows NT!"), 
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("Connect-the-Points Mouse Demo"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }

return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static POINT pt[MAXPOINTS] ;
     static int   iCount ;
     HDC          hdc ;
     int          i, j ;
     PAINTSTRUCT  ps ;

     switch (message)
     {
     case WM_LBUTTONDOWN:
          iCount = 0 ;
          InvalidateRect (hwnd, NULL, TRUE) ;
          return 0 ;
          
     case WM_MOUSEMOVE:
          if (wParam & MK_LBUTTON && iCount < 1000)
          {
               pt[iCount  ].x = LOWORD (lParam) ;
               pt[iCount++].y = HIWORD (lParam) ;
               
               hdc = GetDC (hwnd) ;
               SetPixel (hdc, LOWORD (lParam), HIWORD (lParam), 0) ;
               ReleaseDC (hwnd, hdc) ;
          }
          return 0 ;
          
     case WM_LBUTTONUP:
          InvalidateRect (hwnd, NULL, FALSE) ;
          return 0 ;
          
     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;
          
          SetCursor (LoadCursor (NULL, IDC_WAIT)) ;
          ShowCursor (TRUE) ;
          
          for (i = 0 ; i < iCount - 1 ; i++)
               for (j = i + 1 ; j < iCount ; j++)
               {
                    MoveToEx (hdc, pt[i].x, pt[i].y, NULL) ;
                    LineTo   (hdc, pt[j].x, pt[j].y) ;
               }

          ShowCursor (FALSE) ;
          SetCursor (LoadCursor (NULL, IDC_ARROW)) ;
          EndPaint (hwnd, &ps) ;
          return 0 ;
               
     case WM_DESTROY:
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

CONNECT processes three mouse messages:

Click to view at full size.

Figure 7-2. The CONNECT display.

To use CONNECT, bring the mouse cursor into the client area, press the left button, move the mouse around a little, and then release the left button. CONNECT works best for a curved pattern of a few dots, which you can draw by moving the mouse quickly while the left button is depressed.

CONNECT uses three GDI function calls that I discussed in Chapter 5: SetPixel draws a black pixel for each WM_MOUSEMOVE message when the left mouse button is depressed. (On high-resolution displays, these pixels might be nearly invisible.) Drawing the lines requires MoveToEx and LineTo.

If you move the mouse cursor out of the client area before releasing the button, CONNECT does not connect the dots because it doesn't receive the WM_LBUTTONUP message. If you move the mouse back into the client area and press the left button again, CONNECT clears the client area. If you want to continue a design after releasing the button outside the client area, press the left button again while the mouse is outside the client area and then move the mouse back inside.

CONNECT stores a maximum of 1000 points. If the number of points is P, the number of lines CONNECT draws is equal to P × (P - 1) / 2. With 1000 points, this involves almost 500,000 lines, which might take a minute or so to draw, depending on your hardware. Because Windows 98 is a preemptive multitasking environment, you can switch to other programs at this time. However, you can't do anything else with the CONNECT program (such as move it or change the size) while the program is busy. In Chapter 20, we'll examine methods for dealing with problems such as this.

Because CONNECT might take some time to draw the lines, it switches to an hourglass cursor and then back again while processing the WM_PAINT message. This requires two calls to the SetCursor function using two stock cursors. CONNECT also calls ShowCursor twice, once with a TRUE parameter and the second time with a FALSE parameter. I'll discuss these calls in more detail later in this chapter, in the section "Emulating the Mouse with the Keyboard".

Sometimes the word "tracking" is used to refer to the way that programs process mouse movement. Tracking does not mean, however, that your program sits in a loop in its window procedure while attempting to follow the mouse's movements on the display. The window procedure instead processes each mouse message as it comes and then quickly returns control to Windows.

Processing Shift Keys

When CONNECT receives a WM_MOUSEMOVE message, it performs a bitwise AND operation on the value of wParam and MK_LBUTTON to determine if the left button is depressed. You can also use wParam to determine the state of the Shift keys. For instance, if processing must be dependent on the status of the Shift and Ctrl keys, you might use logic that looks like this:

if (wParam & MK_SHIFT)
{
     if (wParam & MK_CONTROL) 
     {
          [Shift and Ctrl keys are down]
     }
     else
     {
          [Shift key is down]
     }
{
else
{
     if (wParam & MK_CONTROL]
     {
           [Ctrl key is down]
     }
     else
     {
          [neither Shift nor Ctrl key is down]
     }
}

If you want to use both the left and right mouse buttons in your program, and if you also want to accommodate those users with a one-button mouse, you can write your code so that Shift in combination with the left button is equivalent to the right button. In that case, your mouse button-click processing might look something like this:

case WM_LBUTTONDOWN:
     if (!(wParam & MK_SHIFT))
     {
          [left button logic]
          return 0 ;
     }
                    // Fall through
case WM_RBUTTONDOWN:
     [right button logic]
     return 0 ;

The Window function GetKeyState (described in Chapter 6) can also return the status of the mouse buttons or shift keys using the virtual key codes VK_LBUTTON, VK_RBUTTON, VK_MBUTTON, VK_SHIFT, and VK_CONTROL. The button or key is down if the value returned from GetKeyState is negative. Because GetKeyState returns mouse or key states as of the message currently being processed, the status information is properly synchronized with the messages. Just as you cannot use GetKeyState for a key that has yet to be pressed, you cannot use it for a mouse button that has yet to be pressed. Don't do this:

while (GetKeyState (VK_LBUTTON) >= 0) ;  // WRONG !!!

The GetKeyState function will report that the left button is depressed only if the button is already depressed when you process the message during which you call GetKeyState.

Mouse Double-Clicks

A mouse double-click is two clicks in quick succession. To qualify as a double-click, the two clicks must occur in close physical proximity of one another (by default, about an area as wide as an average system font character and half as high) and within a specific interval of time called the "double-click speed." You can change that time interval in the Control Panel.

If you want your window procedure to receive double-click mouse messages, you must include the identifier CS_DBLCLKS when initializing the style field in the window class structure before calling RegisterClass:

wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS ;

If you do not include CS_DBLCLKS in the window style and the user clicks the left mouse button twice in quick succession, your window procedure receives these messages:

WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDOWN
WM_LBUTTONUP

The window procedure might also receive other messages between these button messages. If you want to implement your own double-click logic, you can use the Windows function GetMessageTime to obtain the relative times of the WM_LBUTTONDOWN messages. This function is discussed in more detail in Chapter 8.

If you include CS_DBLCLKS in your window class style, the window procedure receives these messages for a double-click:

WM_LBUTTONDOWN

WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_LBUTTONUP

The WM_LBUTTONDBLCLK message simply replaces the second WM_LBUTTONDOWN message.

Double-click messages are much easier to process if the first click of a double-click performs the same action as a single click. The second click (the WM_LBUTTONDBLCLK message) then does something in addition to the first click. For example, look at how the mouse works with the file lists in Windows Explorer. A single click selects the file. Windows Explorer highlights the file with a reverse-video bar. A double-click performs two actions: the first click selects the file, just as a single click does; the second click directs Windows Explorer to open the file. That's fairly easy logic. Mouse-handling logic could get more complex if the first click of a double-click did not perform the same action as a single click.