Capítulo 33 El ratón

Aunque importante, se considera que el ratón no es imprescindible en Windows, por lo tanto, debemos incluir todo lo necesario para que nuestras aplicaciones se puedan manejar exclusivamente con el teclado. Esta es la recomendación de Windows, sin embargo, no todo el mundo la sigue, y a menudo (cada vez más) encontramos aplicaciones que no pueden manejarse sin ratón.

Todas las entradas procedentes del ratón se reciben mediante mensajes. Así que si nuestra aplicación quiere procesar el ratón como una entrada, debe procesar esos mensajes.

Como vimos en el capítulo anterior, el ratón está asociado al cursor, cuando el primero se mueve, el segundo se desplaza en pantalla para indicar dicho movimiento. Tenemos esto tan asumido que frecuentemente decimos que movemos tanto el cursor como el ratón indistintamente. La ventana que recibe los mensajes del ratón es sobre la que se sitúa el punto activo del cursor (hotspot).

Capturar el ratón

Como si fuesemos un gato, podemos capturar el ratón y mantenerlo cautivo para nuestra aplicación usando la función SetCapture e indicando qué ventana es la que captura el ratón. Para liberarlo se puede usar la función ReleaseCapture, pero también se liberará si otra ventana captura el ratón o si el usuario hace clic en otra ventana distinta de la que lo ha capturado.

Cada vez que el ratón es capturado, se envía el mensaje WM_CAPTURECHANGED a la ventana que pierde la captura.

Un caso típico de captura de ratón es el del arrastre de objetos de una ventana a otra, por ejemplo, pulsamos el botón izquierdo del ratón sobre el icono correspondiente a un fichero, y manteniéndolo pulsado movemos el cursor a otra ventana, sólo entonces soltamos el botón. Si queremos que la primera ventana siga recibiendo los mensajes del ratón aunque el cursor salga de sus límites, incluido el mensaje de soltar el botón, deberemos capturar el ratón.

No todos los mensajes sobre eventos del ratón son enviados a la ventana que lo ha capturado. Por ejemplo si el cursor se desplaza sobre ventanas diferentes a la que ha capturado el ratón, los mensajes sobre el movimiento del cursor se envían a esas ventanas, salvo que uno de los botones del ratón permanezca pulsado.

Otro efecto secundario destacable es que también perderemos las funciones normales del ratón sobre las ventanas hijas de la que ha capturado el ratón. Es decir, si capturamos el ratón, los clics sobre controles o menús de la ventana no realizan sus acciones habituales. De modo que no podremos acceder al menú, ni activar controles mediante el ratón.

Configuración

Para saber si el ratón está presente se puede usar la función GetSystemMetrics, con el parámetro SM_MOUSEPRESENT. Además, podemos averiguar el número de botones del ratón, usando la misma función, con el parámetro SM_CMOUSEBUTTONS. Se puede trabajar con ratones de uno, dos o tres botones. Los llamaremos izquierdo, derecho y central, y frecuentemente apareceran con sus inicales en inglés: L, R y M, respectivamente.

Las funciones de los botones izquierdo y derecho se pueden intercambiar cuando el usuario lo maneja con la mano izquierda (o si quiere hacerlo), mediante la función SwapMouseButton. Esto significa que el botón izquierdo genera los mensajes del botón derecho, y viceversa.

Hay que tener en cuenta que el ratón es un recurso compartido, por lo tanto, esta modificación afectará a todas las ventanas.

Mensajes

Cuando ocurre un evento relacionado con el ratón: pulsaciones de botones o movimientos, se envía un mensaje, y junto con él, las coordenadas del punto activo del cursor. Además, las ventanas siguen recibiendo estos mensajes aunque no tengan el foco del teclado. También los recibirán si la ventana ha capturado el ratón, aunque el cursor no esté sobre la ventana.

Los mensajes del ratón se envían del modo "post", es decir, son mensajes "lentos". En realidad se colocan en una cola que se procesa cuando el sistema tiene tiempo libre. Si se generan muchos mensajes en poco tiempo, el sistema elimina automáticamente los más antiguos, de modo que la aplicación sólo recibe los últimos.

Los mensajes "rápidos" se envían en modo "send", y el control pasa directamente al procedimiento de ventana, es decir, todos los mensajes de este tipo se procesan.

Nota: lo siento, pero no he encontrado traducción para los términos "send" y "post" que indiquen todos los matices implícitos en inglés. Podríamos decir que los mensajes enviados "send" viajan por teléfono, el receptor los recibe tan pronto se generan, los del tipo "post" viajan por correo, y es posible que los últimos mensajes invaliden los anteriores o sencillamente, los hagan inútiles.

Mensajes del área de cliente

Normalmente sólo procesaremos estos mensajes, e ignoraremos el resto.

El mensaje WM_MOUSEMOVE se recibe cada vez que el usuario mueve el cursor sobre la ventana. Este mensaje es el que más frecuentemente se elimina, ya que puede ser generado muchas veces por segundo.

Existen otros mensajes, uno por cada evento de pulsar o soltar un botón, o por un doble clic, y para cada uno de estos tres eventos, variantes para cada uno de los tres botones del ratón. En total nueve mensajes.

WM_LBUTTONDOWN, WM_MBUTTONDOWN y WM_RBUTTONDOWN se envían cada vez que el usuario pulsa el botón izquierdo, central o derecho, respectivamente.

WM_LBUTTONUP, WM_MBUTTONUP y WM_RBUTTONUP se envían cuando el usuario suelta cada uno de los botones izquierdo, central o derecho, respectivamente.

WM_LBUTTONDBLCLK, WM_MBUTTONDBLCLK y WM_RBUTTONDBLCLK se envían cuando el usuario hace doble clic sobre el botón izquierdo, central o derecho respectivamente. En estos casos también se envían los mensajes correspondientes a las pulsaciones y sueltas individuales que completan el doble clic. Por ejemplo, un mensaje WM_LBUTTONDBLCLK, se recibe dentro de una secuencia de mensajes WM_LBUTTONDOWN, WM_LBUTTONUP, WM_LBUTTONDBLCLK y WM_LBUTTONUP.

Para que se genere un mensaje de doble clic se deben cumplir ciertos requisitos. El segundo clic debe producirse en un área determinada alrededor del primero, y dentro de un intervalo de tiempo determinado. Tanto las dimensiones del área (en realidad un rectángulo), como el tiempo de doble clic se pueden modificar mediante funciones del API, pero es mejor dejar este trabajo al usuario mediante Panel de Control, ya que estos cambios afectan a todas las ventanas.

Las ventanas no reciben los mensajes de doble clic por defecto, hay que activar un estilo determinado para que esto sea así. En concreto, al ventana debe crearse a partir de una clase que tenga el estilo CS_DBLCLKS. Hasta ahora siempre hemos creado ventanas de este tipo en nuestros ejemplos:

    WNDCLASSEX wincl;        /* Estructura de datos para la clase de ventana */

    /* Estructura de la ventana */
    wincl.hInstance = hThisInstance;
    wincl.lpszClassName = "NUESTRA_CLASE";
    wincl.lpfnWndProc = WindowProcedure;      /* Esta función es invocada por Windows */
    wincl.style = CS_DBLCLKS;                 /* Captura los doble-clicks */
    wincl.cbSize = sizeof (WNDCLASSEX);

    /* Usar icono y puntero por defector */
    wincl.hIcon = LoadIcon (hThisInstance, "Icono");
    wincl.hIconSm = LoadIcon (hThisInstance, "Icono");
    wincl.hCursor = NULL;
    wincl.lpszMenuName = "Menu";
    wincl.cbClsExtra = 0;    /* Sin información adicional para la */
    wincl.cbWndExtra = 0;    /* clase o la ventana */
    /* Usar el color de fondo por defecto para es escritorio */
    wincl.hbrBackground =  GetSysColorBrush(COLOR_BACKGROUND);

    /* Registrar la clase de ventana, si falla, salir del programa */
    if(!RegisterClassEx(&wincl)) return 0;

En todos los casos, en el parámetro lParam de cada mensaje se reciben las coordenadas del punto activo del cursor. En la palabra de menor peso la coordenada x y en la de mayor peso, la coordenada y. Además, las coordenadas son coordenadas de cliente, es decir, relativas a la esquina superior izquierda del área de cliente. Para separar estos valores se puede usar la macro MAKEPOINTS, que convierte el valor en el parámetro lParam en una estructura POINTS.

También en todos los casos, el parámetro wParam del mensaje contiene información sobre si ciertas teclas o botones del ratón están pulsados.

Valor Descripción
MK_CONTROL Activo si la tecla CTRL está pulsada.
MK_LBUTTON Activo si el botón izquierdo del ratón está pulsado.
MK_MBUTTON Activo si el botón central del ratón está pulsado.
MK_RBUTTON Activo si el botón derecho del ratón está pulsado.
MK_SHIFT Activo si la tecla MAYÚSCULAS está pulsada.
    static int izq, cen, der;
    static POINTS punto;
...
        case WM_MOUSEMOVE:
           punto = MAKEPOINTS(lParam);
           izq = (wParam & MK_LBUTTON) ? 1 : 0;
           cen = (wParam & MK_MBUTTON) ? 1 : 0;
           der = (wParam & MK_RBUTTON) ? 1 : 0;
           hdc = GetDC(hwnd);
           Pintar(hdc, izq, cen, der, punto, TRUE);
           ReleaseDC(hwnd, hdc);
           break;
        case WM_LBUTTONDOWN:
           izq = 2;
           punto = MAKEPOINTS(lParam);
           hdc = GetDC(hwnd);
           Pintar(hdc, izq, cen, der, punto, TRUE);
           ReleaseDC(hwnd, hdc);
           break;
        case WM_LBUTTONUP:
           izq = 3;
           punto = MAKEPOINTS(lParam);
           hdc = GetDC(hwnd);
           Pintar(hdc, izq, cen, der, punto, TRUE);
           ReleaseDC(hwnd, hdc);
           break;
        case WM_LBUTTONDBLCLK:
           izq = 4;
           punto = MAKEPOINTS(lParam);
           hdc = GetDC(hwnd);
           Pintar(hdc, izq, cen, der, punto, TRUE);
           ReleaseDC(hwnd, hdc);
           break;

Mensajes del área de no cliente

Existe la misma gama de mensajes de ratón para el área de no cliente que para el área de cliente, el nombre de los mensajes es el mismo, pero con la letras NC después del '_', por ejemplo, el mensaje que notifica movimientos del cursor dentro del área de no cliente es WM_NCMOUSEMOVE.

Del mismo modo, los mensajes relacionados con clics de botones son:

WM_NCLBUTTONDOWN, WM_NCMBUTTONDOWN y WM_NCRBUTTONDOWN se envían cada vez que el usuario pulsa el botón izquierdo, central o derecho, respectivamente.

WM_NCLBUTTONUP, WM_NCMBUTTONUP y WM_NCRBUTTONUP se envían cuando el usuario suelta cada uno de los botones izquierdo, central o derecho, respectivamente.

WM_NCLBUTTONDBLCLK, WM_NCMBUTTONDBLCLK y WM_NCRBUTTONDBLCLK se envían cuando el usuario hace doble clic sobre el botón izquierdo, central o derecho respectivamente. En estos casos también se envían los mensajes correspondientes a las pulsaciones y sueltas individuales que completan el doble clic. Por ejemplo, un mensaje WM_NCLBUTTONDBLCLK, se recibe dentro de una secuencia de mensajes WM_NCLBUTTONDOWN, WM_NCLBUTTONUP, WM_NCLBUTTONDBLCLK y WM_NCLBUTTONUP.

Estos mensajes se deben procesar con cuidado, ya que al hacer que la aplicación responda a ellos podremos perder muchas de las funciones propias del área de no cliente, como acceso a menús, movimiento de la ventana, cambio de tamaño, etc, salvo que llamemos al procedimiento de ventana por defecto después de procesar el mensaje. Pero generalmente no tendremos necesidad de procesar estos mensajes.

En el caso de estos mensajes, el parámetro wParam no contiene información sobre el estado de teclas y botones, sino sobre el hit test, la zona en la que se encuentra el punto activo del cursor. De este modo podemos saber si está sobre un borde, un menú, la barra de título, etc. Veremos esto en más detalle más abajo, al ver el mensaje WM_NCHITTEST.

El parámetros lParam sigue conteniendo las coordenadas del cursor, pero en coordenadas de pantalla.

        case WM_NCMOUSEMOVE:
           punto = MAKEPOINTS(lParam);
           izq = cen = der = 0;
           hdc = GetDC(hwnd);
           Pintar(hdc, izq, cen, der, punto, FALSE);
           ReleaseDC(hwnd, hdc);
           break;
        case WM_NCLBUTTONDOWN:
           izq = 2;
           punto = MAKEPOINTS(lParam);
           hdc = GetDC(hwnd);
           Pintar(hdc, izq, cen, der, punto, FALSE);
           ReleaseDC(hwnd, hdc);
           return DefWindowProc(hwnd, msg, wParam, lParam);
           break;
        case WM_NCLBUTTONUP:
           izq = 3;
           punto = MAKEPOINTS(lParam);
           hdc = GetDC(hwnd);
           Pintar(hdc, izq, cen, der, punto, FALSE);
           ReleaseDC(hwnd, hdc);
           return DefWindowProc(hwnd, msg, wParam, lParam);
           break;
        case WM_NCLBUTTONDBLCLK:
           izq = 4;
           punto = MAKEPOINTS(lParam);
           hdc = GetDC(hwnd);
           Pintar(hdc, izq, cen, der, punto, FALSE);
           ReleaseDC(hwnd, hdc);
           return DefWindowProc(hwnd, msg, wParam, lParam);
           break;

Mensaje WM_NCHITTEST

El mensaje WM_NCHITTEST se envía a la ventana que contiene el cursor, o a la que ha capturado el ratón, cada vez que ocurre un evento del ratón. Este mensaje se usa por Windows para determinar si se debe enviar un nuevo mensaje de área de cliente o de área de no cliente. Si queremos que nuestra aplicación reciba mensajes del ratón debemos dejar que la función DefWindowProc procese este mensaje.

    static POINTS puntoHT;
    static LRESULT hittest;
... 
        case WM_NCHITTEST:
           puntoHT = MAKEPOINTS(lParam);
           hdc = GetDC(hwnd);
           sprintf(cad, "Punto HIT-TEST: (%4d, %4d)",
              puntoHT.x, puntoHT.y);
           TextOut(hdc, 10, 110, cad, strlen(cad));
           MostrarHitTest(hdc, hittest);
           ReleaseDC(hwnd, hdc);
           hittest = DefWindowProc(hwnd, msg, wParam, lParam);
           return hittest;

En el parámetro lParam se reciben las coordenadas del punto activo del cursor en coordenadas de pantalla:

Cuando la función DefWindowProc procesa este mensaje devuelve un código de hit-test, que depende de la zona en que se encuentre el cursor.

Valor Posición del punto activo
HTBORDER En el borde de la ventana que no tiene borde de cambio de tamaño.
HTBOTTOM En borde horizontal inferior de una ventana.
HTBOTTOMLEFT En la esquina inferior izquierda del borde de una ventana.
HTBOTTOMRIGHT En la esquina inferior derecha del borde de una ventana.
HTCAPTION En una barra de título.
HTCLIENT En un área de cliente.
HTERROR En el fondo de la pantalla o en una línea de división entre ventanas (lo mismo que HTNOWHERE, excepto que DefWindowProc produce un pitido de sistema para indicar un error).
HTGROWBOX En una caja de cambio de tamaño (lo mismo que HTSIZE).
HTHSCROLL En la barra de desplazamiento horizontal.
HTLEFT En el borde izquierdo de una ventana.
HTMENU En un menú.
HTNOWHERE En el fondo de la pantalla o en una línea de división entre ventanas.
HTREDUCE En un botón de minimizar.
HTRIGHT En el borde derecho de una ventana.
HTSIZE En una caja de cambio de tamaño (lo mismo que HTGROWBOX).
HTSYSMENU En un menú de sistema o en un botón de cierre en una ventana hija.
HTTOP En el borde horizontal superior de una ventana.
HTTOPLEFT En la esquina superior izquierda del borde de una ventana.
HTTOPRIGHT En la esquina superior derecha de un borde de ventana.
HTTRANSPARENT En una ventana actualmente tapada por otra ventana.
HTVSCROLL En la barra de desplazamiento vertical.
HTZOOM En un botón de maximizar.

Si el valor devuelto por el procedimiento de ventana es HTCLIENT es porque está en el área de cliente, en ese caso las coordenadas se trasladan a coordenadas de cliente y se envía un mensaje "lento "de área de cliente, y en el parámetro wParam se envía el estado de los botones del ratón. Si se encuentra en otra zona, se envía un mensaje "lento" de área de no cliente, se mantienen las coordenadas en coordenadas de pantalla y en el parámetro wParam se envía el código hit-test.

Mensaje WM_MOUSEACTIVATE

El mensaje WM_MOUSEACTIVATE se envía a una ventana cuando se hace clic con uno de los botones del ratón cuando el cursor está sobre ella o sobre una de sus ventanas hijas, y si la ventana está inactiva. Este mensaje se envía después del mensaje WM_NCHITTEST y antes de cualquier mensaje de área de cliente o de área de ni cliente.

Si se deja que DefWindowProc procese este mensaje, se activará la ventana y se enviará el mensaje de pulsación de botón a la ventana.

Si procesamos el mensaje en nuestro procedimiento de ventana tenemos más opciones, y podemos controlar si se activa o no la ventana y si se descarta el mensaje de pulsación del botón o no. Podemos devolver los siguientes valores para conseguir estos resultados al procesar este mensaje:

Valor Significado
MA_ACTIVATE Activa la ventana, y no descarta el mensaje de ratón.
MA_ACTIVATEANDEAT Activa la ventana, y descarta el mensaje de ratón.
MA_NOACTIVATE No activa la ventana, y no descarta el mensaje de ratón.
MA_NOACTIVATEANDEAT No activa la ventana, pero descarta el mensaje de ratón.