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.

Otros mensajes de ratón

Algunas de las características que ahora son corrientes en el manejo del ratón, no lo eran o ni siquiera existían hace unos años. Por ejemplo, la rueda del ratón es un invento relativamente reciente. Además, debido a los navegadores de Internet, se han popularizado algunos eventos relacionados con el ratón que antes no existían, como el paso sobre zonas determinadas, o la transición entre unas zonas y otras de la pantalla. Estos eventos se usan en los navegadores para cambiar el aspecto de textos o botones, y de ese modo llamar la atención del usuario sobre ellos para indicar que son zonas activas a clics.

Veamos ahora estos nuevos mensajes en el API.

Mensaje WM_MOUSEWHEEL (Windows NT)

El mensaje WM_MOUSEWHEEL se envía cada vez que el usuario activa la rueda de desplazamiento del ratón. Los parámetros de este mensaje son los mismos que en el mensaje WM_MOUSEMOVE, pero para incluir la información sobre el avance o retroceso de la rueda, el parámetro wParam se ha dividido en dos. En la parte baja se empaquetan las banderas sobre el estado de los botones, y en la parte alta el valor de avance o retroceso de la rueda. Usaremos las macros LOWORD y HIWORD para extraer esos valores.

Los valores de avance y retroceso de la rueda serán siempre múltiplos de 120. Esto se ha hecho así para permitir que en el futuro se puedan construir dispositivos compatibles con la rueda, pero que proporcionen mayor precisión. Debemos considerar siempre que una unidad de desplazamiento de la rueda es 120, o para evitar problemas de dependencias, de la constante WHEEL_DELTA.

Los valores positivos indican movimientos de rueda hacia adelante, y los negativos, hacia atrás.

        case WM_MOUSEWHEEL:
           punto = MAKEPOINTS(lParam);
           izq = (LOWORD(wParam) & MK_LBUTTON) ? 1 : 0;
           cen = (LOWORD(wParam) & MK_MBUTTON) ? 1 : 0;
           der = (LOWORD(wParam) & MK_RBUTTON) ? 1 : 0;
           rueda = HIWORD(wParam);
           rueda /= WHEEL_DELTA;
           hdc = GetDC(hwnd);
           Pintar(hdc, izq, cen, der, punto, TRUE);
           sprintf(cad, "rueda = %04d ", rueda);
           TextOut(hdc, 10, 130, cad, strlen(cad));
           ReleaseDC(hwnd, hdc);
           break;

Trazar eventos del ratón (Windows NT)

Nuestra aplicación puede recibir dos mensajes sobre eventos del ratón: WM_MOUSELEAVE y WM_MOUSEHOVER, cuando el ratón abandona una ventana o cuando permanece sobre un área determinada de la ventana durante un periodo de tiempo, respectivamente. Pero para que esto suceda, previamente debemos activar el trazado de eventos, mediante una llamada a la función TrackMouseEvent.

Esta función necesita como parámetro un puntero a una estructura TRACKMOUSEEVENT. En esa estructura indicamos qué eventos queremos que se notifique, y el tiempo necesario para generar un mensaje WM_MOUSEHOVER:

        case WM_NCHITTEST:
           hittest = DefWindowProc(hwnd, msg, wParam, lParam);
           if(HTCLIENT == hittest) {
              if(!dentro) {
                 dentro = TRUE;
                 tme.cbSize = sizeof(TRACKMOUSEEVENT);
                 tme.dwFlags = TME_HOVER | TME_LEAVE;
                 tme.hwndTrack = hwnd;
                 tme.dwHoverTime = 1000;
                 TrackMouseEvent(&tme);
              }
           }
           return hittest;
           break;

En este ejemplo llamamos a TrackMouseEvent para que nos notifique ambos eventos para la ventana hwnd, y ajustamos el tiempo Hover en un segundo.

Mensaje WM_MOUSELEAVE (Windows NT)

Si hemos activado el evento Leave recibiremos un mensaje WM_MOUSELEAVE cuando el cursor abandone el área de cliente de la ventana. Una vez que se recibe el mensaje no se vuelve a generar, salvo que volvamos a activar el evento, usando de nuevo la función TrackMouseEvent.

        case WM_MOUSELEAVE:
           dentro = FALSE;
           hdc = GetDC(hwnd);
           TextOut(hdc, 10, 170, "Leave", 5);
           ReleaseDC(hwnd, hdc);
           break;

Mensaje WM_MOUSEHOVER (Windows NT)

Del mismo modo, si hemos activado el evento Hover recibiremos un mensaje WM_MOUSEHOVER cuando haya transcurrido el tiempo indicado y el cursor no se haya movido de una zona determinada del área de cliente de la ventana. Esta zona está predederminada por Windows, aunque se puede modificar su tamaño (ver TRACKMOUSEEVENT. Una vez que se recibe el mensaje no se vuelve a generar, salvo que volvamos a activar el evento, usando de nuevo la función TrackMouseEvent.

        case WM_MOUSEHOVER:
           hdc = GetDC(hwnd);
           TextOut(hdc, 10, 170, "Hover", 5);
           ReleaseDC(hwnd, hdc);
           break;

Ejemplo 34

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 34 win034.zip 2004-07-06 5809 bytes 195

Arrastrar objetos

Una de las operaciones más frecuentes que se realizan mediante el ratón es la de arrastrar objetos. No entraremos en muchos detalles por ahora, Windows dispone de formas especiales de realizar arrastre de objetos entre distintas ventanas y aplicaciones, pero de momento veremos un ejemplo sencillo sobre cómo arrastrar objetos dentro de una misma ventana.

En este ejemplo usaremos iconos como objetos. A cada icono le corresponde una imagen y una posición en pantalla:

typedef struct {
    HICON icono;
    POINT coordenada;
} Objeto;

Al procesar el mensaje WM_CREATE inicializamos el array de objetos:

        case WM_CREATE:
           hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
           objeto[0].icono = LoadIcon(hInstance, "ufo");
           objeto[0].coordenada.x = 10;
           objeto[0].coordenada.y = 10;
           objeto[1].icono = LoadIcon(hInstance, "libro");
           objeto[1].coordenada.x = 10;
           objeto[1].coordenada.y = 50;
           objeto[2].icono = LoadIcon(hInstance, "mundo");
           objeto[2].coordenada.x = 10;
           objeto[2].coordenada.y = 90;
           objeto[3].icono = LoadIcon(hInstance, "hamburguesa");
           objeto[3].coordenada.x = 50;
           objeto[3].coordenada.y = 10;
           objeto[4].icono = LoadIcon(hInstance, "smile");
           objeto[4].coordenada.x = 50;
           objeto[4].coordenada.y = 50;
           capturado = -1;
           break;

Una operación de arrastre comienza con un clic sobre un objeto. Después, sin soltar el botón del ratón, se desplaza el ratón, y con él el obejto, a la posición deseada, y finalmente, se suelta el botón del ratón en esa posición.

Lo primero es seleccionar el objeto a arrastrar. Para ello procesaremos el mensaje WM_LBUTTONDOWN, y comprobaremos si las coordenadas del cursor corresponden con alguno de los objetos que es posible arrastrar. Si es así, guardamos en una variable el identificador del objeto, y al mismo tiempo, activamos el modo de arrastre:

        case WM_LBUTTONDOWN:
           punto = MAKEPOINTS(lParam);
           for(i = 0; capturado == -1 && i < 5; i++) {
              SetRect(&re,
                objeto[i].coordenada.x,
                objeto[i].coordenada.y,
                objeto[i].coordenada.x+32,
                objeto[i].coordenada.y+32);
              POINTSTOPOINT(lpunto, punto);
              if(PtInRect(&re, lpunto)) {
                 capturado = i;
                 ClientToScreen(hwnd, &objeto[i].coordenada);
                 SetCursorPos(objeto[i].coordenada.x,
                    objeto[i].coordenada.y);
                 SetCapture(hwnd);
                 ShowCursor(FALSE);
              }
           }
           break;

Si la variable capturado vale -1, indica que no se está realizando un arrastre, si tiene otro valor, indica el objeto arrestrado.

Para cada objeto calculamos las coordenadas de un rectángulo que lo contiene, y comprobamos si la posición actual del cursor está dentro del rectángulo. Si es así, actualizamos el valor de capturado, cambiamos la posición del cursor a la esquina superior izquierda del objeto, y ocultamos el cursor.

El cambio de coordenadas sirve para eliminar el salto que se produciría cuando pinchemos sobre un punto distinto de la esquina superior izquierda. Ocultar el cursor hace que el objeto arrastrado parezca sustituir al cursor, y hace más fácil arrastrarlo con precisión.

Además, capturamos el ratón para que la operación de arrastre no se interrumpa si el cursor sale del área de cliente de la ventana.

Durante el arrastre debemos procesar el mensaje WM_MOUSEMOVE. Cada vez que recibamos el mensaje, borraremos el objeto en su posición actual, actualizamos las coordenadas según la posición del cursor, y volvemos a dibujarlo en la nueva posición:

        case WM_MOUSEMOVE:
           punto = MAKEPOINTS(lParam);
           if(capturado != -1) {
              hdc = GetDC(hwnd);
              //drag
              // Borrar en posición actual:
              SetRect(&re,
                 objeto[capturado].coordenada.x,
                 objeto[capturado].coordenada.y,
                 objeto[capturado].coordenada.x+32,
                 objeto[capturado].coordenada.y+32);
              FillRect(hdc, &re, GetSysColorBrush(COLOR_BACKGROUND));
              // Actualizar coordenadas:
              POINTSTOPOINT(objeto[capturado].coordenada, punto);
              // Pintar en nueva posición:
              DrawIcon(hdc,
                 objeto[capturado].coordenada.x,
                 objeto[capturado].coordenada.y,
                 objeto[capturado].icono);
              ReleaseDC(hwnd, hdc);
              InvalidateRect(hwnd, &re, FALSE);
           }
           break;

Para borrar pintamos usando el color de fondo, la zona ocupara por el objeto. Para actualizar la posición del objeto usamos la macro POINTSTOPOINT, esto es porque la posición del cursor se almacena en una estructura POINTS, y la del objeto en una estructura POINT. A continuación mostramos el objeto, e invalidamos la zona que ocupaba originalmente. Esto último es necesario, ya que el objeto arrastrado puede pasar sobre otros objetos borrándolos.

Finalmente, cuando soltemos el botón se recibirá un mensaje WM_LBUTTONUP. En ese momento debemos dar por terminada la operación de arrastre, y regresaremos al estado inicial:

        case WM_LBUTTONUP:
           if(capturado != -1) {
              capturado = -1;
              InvalidateRect(hwnd, NULL, FALSE);
              ReleaseCapture();
              ShowCursor(TRUE);
           }
           break;

Asignamos -1 a capturado, redibujamos toda la ventana, libreramos el ratón y mostramos el cursor.

Ejemplo 35

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 35 win035.zip 2004-07-06 7833 bytes 196