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

Capturing the Mouse

A window procedure normally receives mouse messages only when the mouse cursor is positioned over the client or nonclient area of the window. A program might need to receive mouse messages when the mouse is outside the window. If so, the program can "capture" the mouse. Don't worry: it won't bite.

Blocking Out a Rectangle

To examine why capturing the mouse might be necessary, let's look at the BLOKOUT1 program shown in Figure 7-9. This program may seem functional, but it has a nasty flaw.

Figure 7-9. The BLOKOUT1 program.

BLOKOUT1.C

/*-----------------------------------------
   BLOKOUT1.C -- Mouse Button Demo Program
                 (c) Charles Petzold, 1998

  -----------------------------------------*/

#include <windows.h>

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

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("BlokOut1") ;
     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 ("Mouse Button 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 ;
}

void DrawBoxOutline (HWND hwnd, POINT ptBeg, POINT ptEnd)
{
     HDC hdc ;
     
     hdc = GetDC (hwnd) ;
     
     SetROP2 (hdc, R2_NOT) ;
     SelectObject (hdc, GetStockObject (NULL_BRUSH)) ;
     Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ;
     
     ReleaseDC (hwnd, hdc) ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static BOOL  fBlocking, fValidBox ;
     static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd ;
     HDC          hdc ;
     PAINTSTRUCT  ps ;
     
     switch (message)
     {
     case WM_LBUTTONDOWN :
          ptBeg.x = ptEnd.x = LOWORD (lParam) ;
          ptBeg.y = ptEnd.y = HIWORD (lParam) ;
          
          DrawBoxOutline (hwnd, ptBeg, ptEnd) ;
          
          SetCursor (LoadCursor (NULL, IDC_CROSS)) ;
          
          fBlocking = TRUE ;
          return 0 ;

     case WM_MOUSEMOVE :
          if (fBlocking)
          {
               SetCursor (LoadCursor (NULL, IDC_CROSS)) ;
               
               DrawBoxOutline (hwnd, ptBeg, ptEnd) ;
               
               ptEnd.x = LOWORD (lParam) ;
               ptEnd.y = HIWORD (lParam) ;
               
               DrawBoxOutline (hwnd, ptBeg, ptEnd) ;
          }
          return 0 ;
          
     case WM_LBUTTONUP :
          if (fBlocking)
          {
               DrawBoxOutline (hwnd, ptBeg, ptEnd) ;
               
               ptBoxBeg   = ptBeg ;
               ptBoxEnd.x = LOWORD (lParam) ;
               ptBoxEnd.y = HIWORD (lParam) ;
               
               SetCursor (LoadCursor (NULL, IDC_ARROW)) ;

               fBlocking = FALSE ;
               fValidBox  = TRUE ;
               
               InvalidateRect (hwnd, NULL, TRUE) ;
          }
          return 0 ;
          
     case WM_CHAR :
          if (fBlocking & wParam == '\x1B')       // i.e., Escape
          {
               DrawBoxOutline (hwnd, ptBeg, ptEnd) ;
               
               SetCursor (LoadCursor (NULL, IDC_ARROW)) ;
               
               fBlocking = FALSE ;
          }
          return 0 ;

     case WM_PAINT :
          hdc = BeginPaint (hwnd, &ps) ;

          if (fValidBox)
          {
               SelectObject (hdc, GetStockObject (BLACK_BRUSH)) ;
               Rectangle (hdc, ptBoxBeg.x, ptBoxBeg.y,
                    ptBoxEnd.x, ptBoxEnd.y) ;
          }
          
          if (fBlocking)
          {
               SetROP2 (hdc, R2_NOT) ;
               SelectObject (hdc, GetStockObject (NULL_BRUSH)) ;
               Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ;
          }
          
          EndPaint (hwnd, &ps) ;
          return 0 ;
          
     case WM_DESTROY :
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

This program demonstrates a little something that might be implemented in a Windows drawing program. You begin by depressing the left mouse button to indicate one corner of a rectangle. You then drag the mouse. The program draws an outlined rectangle with the opposite corner at the current mouse position. When you release the mouse, the program fills in the rectangle. Figure 7-10 shows one rectangle already drawn and another in progress.

Click to view at full size.

Figure 7-10. The BLOKOUT1 display.

So, what's the problem?

Try this: Press the left mouse button within BLOKOUT1's client area and then move the cursor outside the window. The program stops receiving WM_MOUSEMOVE messages. Now release the button. BLOKOUT1 doesn't get that WM_BUTTONUP message because the cursor is outside the client area. Move the cursor back within BLOKOUT1's client area. The window procedure still thinks the button is pressed.

This is not good. The program doesn't know what's going on.

The Capture Solution

BLOKOUT1 shows some common program functionality, but the code is obviously flawed. This is the type of problem for which mouse capturing was invented. If the user is dragging the mouse, it should be no big deal if the cursor drifts out of the window for a moment. The program should still be in control of the mouse.

Capturing the mouse is easier than baiting a mousetrap. You need only call

SetCapture (hwnd) ;

After this function call Windows sends all mouse messages to the window procedure for the window whose handle is hwnd. The mouse messages always come through as client-area messages, even when the mouse is in a nonclient area of the window. The lParam parameter still indicates the position of the mouse in client-area coordinates. These x and y coordinates, however, can be negative if the mouse is to the left of or above the client area. When you want to release the mouse, call

ReleaseCapture () ;

which will returns things to normal.

In the 32-bit versions of Windows, mouse capturing is a bit more restrictive than it was in earlier versions of Windows. Specifically, if the mouse has been captured, and if a mouse button is not currently down, and if the mouse cursor passes over another window, the window underneath the cursor will receive the mouse messages rather than the window that captured the mouse. This is necessary to prevent one program from messing up the whole system by capturing the mouse and not releasing it.

To avoid problems, your program should capture the mouse only when the button is depressed in your client area. You should release the capture when the button is released.

The BLOKOUT2 Program

The BLOKOUT2 program that demonstrates mouse capturing is shown in Figure 7-11.

Figure 7-11. The BLOKOUT2 program.

BLOKOUT2.C

/*---------------------------------------------------
   BLOKOUT2.C -- Mouse Button & Capture Demo Program

                 (c) Charles Petzold, 1998
  ---------------------------------------------------*/

#include <windows.h>

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

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("BlokOut2") ;
     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 ("Mouse Button & Capture 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 ;
}

void DrawBoxOutline (HWND hwnd, POINT ptBeg, POINT ptEnd)
{
     HDC hdc ;
     
     hdc = GetDC (hwnd) ;
     
     SetROP2 (hdc, R2_NOT) ;
     SelectObject (hdc, GetStockObject (NULL_BRUSH)) ;
     Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ;
     
     ReleaseDC (hwnd, hdc) ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static BOOL  fBlocking, fValidBox ;
     static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd ;
     HDC          hdc ;
     PAINTSTRUCT  ps ;
     
     switch (message)
     {
     case WM_LBUTTONDOWN :
          ptBeg.x = ptEnd.x = LOWORD (lParam) ;
          ptBeg.y = ptEnd.y = HIWORD (lParam) ;
          
          DrawBoxOutline (hwnd, ptBeg, ptEnd) ;

          SetCapture (hwnd) ;
          SetCursor (LoadCursor (NULL, IDC_CROSS)) ;
          
          fBlocking = TRUE ;
          return 0 ;
          
     case WM_MOUSEMOVE :
          if (fBlocking)
          {
               SetCursor (LoadCursor (NULL, IDC_CROSS)) ;
               
               DrawBoxOutline (hwnd, ptBeg, ptEnd) ;
               
               ptEnd.x = LOWORD (lParam) ;
               ptEnd.y = HIWORD (lParam) ;
               
               DrawBoxOutline (hwnd, ptBeg, ptEnd) ;
          }
          return 0 ;
          
     case WM_LBUTTONUP :
          if (fBlocking)
          {
               DrawBoxOutline (hwnd, ptBeg, ptEnd) ;
               
               ptBoxBeg   = ptBeg ;
               ptBoxEnd.x = LOWORD (lParam) ;
               ptBoxEnd.y = HIWORD (lParam) ;
               
               ReleaseCapture () ;
               SetCursor (LoadCursor (NULL, IDC_ARROW)) ;
               
               fBlocking = FALSE ;
               fValidBox  = TRUE ;
               
               InvalidateRect (hwnd, NULL, TRUE) ;
          }
          return 0 ;
          
     case WM_CHAR :
          if (fBlocking & wParam == '\x1B')       // i.e., Escape
          {
               DrawBoxOutline (hwnd, ptBeg, ptEnd) ;
               ReleaseCapture () ;
               SetCursor (LoadCursor (NULL, IDC_ARROW)) ;

               fBlocking = FALSE ;
          }
          return 0 ;
          
     case WM_PAINT :
          hdc = BeginPaint (hwnd, &ps) ;
          
          if (fValidBox)
          {
               SelectObject (hdc, GetStockObject (BLACK_BRUSH)) ;
               Rectangle (hdc, ptBoxBeg.x, ptBoxBeg.y,
                    ptBoxEnd.x, ptBoxEnd.y) ;
          }
          
          if (fBlocking)
          {
               SetROP2 (hdc, R2_NOT) ;
               SelectObject (hdc, GetStockObject (NULL_BRUSH)) ;
               Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ;
          }
          
          EndPaint (hwnd, &ps) ;
          return 0 ;
          
     case WM_DESTROY :
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

BLOKOUT2 is the same as BLOKOUT1, except with three new lines of code: a call to SetCapture during the WM_LBUTTONDOWN message and calls to ReleaseCapture during the WM_LBUTTONDOWN and WM_CHAR messages. And check this out: Make the window smaller than the screen size, begin blocking out a rectangle within the client area, and then move the mouse cursor outside the client and to the right or bottom, and finally release the mouse button. The program will have the coordinates of the entire rectangle. Just enlarge the window to see it.

Capturing the mouse isn't something suited only for oddball applications. You should do it anytime you need to track WM_MOUSEMOVE messages after a mouse button has been depressed in your client area until the mouse button is released. Your program will be simpler, and the user's expectations will have been met.