Hasta ahora hemos visto cómo implementar una aplicación que definía una clase de ventana, la creaba, y gestionaba los mensajes que se recibían asociados a la misma mediante funciones estándar de la API de Windows. Una vez nuestra ventana es completamente funcional debemos proceder a implementar el código que haga de nuestro programa una aplicación útil. Para ello es necesario que se introduzcan algunos conceptos, como los principios básicos de utilización del GDI, el dispositivo de gráficos de Windows y la recepción de input por parte del usuario.
Por otra parte, se pretende dejar claro que el objetivo del curso no es enseñar a programar aplicaciones bajo Windows (eso se hace de manera muy sencilla con entornos de desarrollo como Delphi o Borland C++ Builder, o incluso las MFC, mucho más visuales), sino introducir al lector en la programación bajo Windows, tan sólo en conceptos que necesitará a la hora de enfrentarse con DirectX, de ahi que dejemos de lado conceptos como ComboBoxes, celdas de texto, etc.
Para escribir en un área cliente necesitamos obtener un handle de contexto de dispositivo a nuestra ventana. Esto puede hacerse de 2 maneras: la primera consiste en utilizar el par de funciones BeginPaint() y EndPaint() cuando estamos procesando un mensaje WM_PAINT, como se vio en la anterior entrega:
hdc = BeginPaint( hwnd, &ps ); // ...llamamos a funciones GDI... EndPaint( hwnd, &ps );La segunda forma nos permite obtener el hdc de la ventana en cualquier parte del programa (no sólo durante un mensaje WM_PAINT), y consiste en utilizar el par de sentencias GetDC() y ReleaseDC(), que permiten obtener y devolver a Windows (son limitados) el hdc de la ventana que le especifiquemos:
hdc = GetDC( hwnd ); // ...acciones GDI... ReleaseDC( hwnd, hdc ) ;Aparte de obtener el handle al área cliente, podemos obtener (si fuese necesario para nuestros propósitos) un handle a toda la ventana (no sólo el área cliente), o incluso a toda la pantalla:
// handle a toda la ventana: hdc = GetWindowDC( hwnd ); // ... funciones GDI... ReleaseDC( hwnd, hdc ); // handle a toda la pantalla: hdc = CreateDC( DISPLAY, NULL, NULL, NULL ); // ... funciones GDI... DeleteDC( hdc );Aparte de la obtención de handles de contexto de dispositivos a ventanas, también podemos crear handles dc de memoria, es decir, crear un hdc para escribir en un buffer de memoria como si fuera una ventana (con las funciones gráficas del GDI de Windows) que después podremos volcar a la ventana real, resultando muy útil para eliminar flickering (parpadeos y otros efectos desagradables) en nuestras aplicaciones.
hdcMem = CreateCompatibleDC( hdc ); // ... funciones GDI sobre hdcMem... BitBlt(hdc, 0, 0, Ancho, Alto, hdcmem, 0, 0, SRCCOPY); DeleteDC( hdcMem );A CreateCompatibleDC() se le pasa el hdc de la ventana con la cual lo queremos hacer compatible (habrá que obtenerlo previamente) de manera que Windows cree el hdc de memoria con la misma profundidad de color (bpp) que la ventana real, de tal modo que el posterior volcado será realizado de una manera más rápida al no realizar conversiones en tiempo real del formato de pixel. En el fragmento de código anterior se ha incluido la función de copia del buffer de memoria a la ventana, aunque la comentaremos a continuación.
COLORREF RGB( r, g, b );
Esta función (de tipo macro) convierte un color RGB especificado en una
referencia al color (lo convierte a 32bpp BGR), de tal manera que, por ej. al
ejecutar RGB(0xFF,0,0xFF) devuelve el valor 0xFF00FF. La importancia de esta
macro radica en que a las funciones del GDI no se les pasa índices de color
(como en 8bpp) ni componentes de color concretas para un modo (como en
15/16/24/32bpp) sino un valor que Windows tratará de encontrar (o al menos, el
más parecido), ya que el hdc deben ser independientes del tipo de dispositivo
gráfico.
COLORREF SetPixel(hdc, X, Y, crColor);
COLORREF GetPixel(hdc, X, Y );
Escribe un pixel del color especificado (crColor) en la posición (X,Y) del
hdc especificado. Análogamente, disponemos de GetPixel(hdc, x, y). Ejemplo:
SetPixel(hdc, 0, 0, RGB(20,1,10)) ;
BOOL LineTo(hdc, nXEnd, nYEnd);
Dibuja una línea desde el punto actual (guardado internamente y modificable
por MoveToEx()) hasta el punto especificado (nXEnd,nYEnd).
BOOL MoveToEx(hdc, X, Y, lpPoint);
Cambia el punto actual para hdc al especificado por (X,Y). El último
parámetro permite almacenar el valor del último punto o bien podemos ignorarlo
poniendolo a NULL si éste no nos interesa.
BOOL BitBlt(hdcDest, nXDest, nYDest, nWidth, nHeight, hdcSrc, nXSrc, nYSrc,
dwRop);
Vuelca un recuadro de pixels (mapa de bits) desde el handle DC
hdcsrc y posición (nXSrc, nYSrc), de tamaño nWidth X nHeight, a la posición
(nXDest,nYDest) del hdc de destino hdcDest. Es decir, es una rutina de trazado
de bitmaps desde HDCORIGEN(x1,y1) hasta HDCDESTINO(x2,y2) de un ancho y alto
determinado. El parámetro dwRop especifica cómo realizar la copia:
BLACKNESS Rellena el rectangulo de destino con el color negro. DSTINVERT Invierte el rectangulo de destino. MERGECOPY Mezcla los colores ORG+DEST usando la función AND. NOTSRCCOPY Copia el rect. Origen invertido. SRCAND Combina ORIGEN+DESTINO con AND. SRCCOPY Copia directamente sin ninguna acción especial. SRCINVERT Combina origen y destino con el operador XOR. SRCPAINT Combina origen+destino con OR. WHITENESS Rellena el rectangulo de destino con el color blanco.Existen multitud más de funciones de dibujo, que pueden ser consultadas en la ayuda del compilador, pero no vamos a extendernos más en este aspecto. Si queréis consultar alguna a modo de referencia, resultan especialmente útiles las siguientes: Ellipse(), TextOut(), Polygon(), Bezier(), Arc(), GetNearestColor(), PolyDraw(), RoundRect(), SetTextColor(), Rectangle(), etc.
También cabe decir que las funciones GDI por defecto tienen recorte automático a nuestra ventana (es decir, si por equivocación escribimos fuera de la misma, Windows hará el recorte para evitarlo), aunque podemos crear superficies de recorte propias.
En el programa de ejemplo del listado 1 puede verse un ejemplo de utilización de las funciones del GDI, trazando la función seno en pantalla. En él puede apreciarse como nos aprovechamos de la función InvalidateRect() para generar un mensaje WM_PAINT y redibujar la pantalla cuando el usuario pulsa ARRIBA o ABAJO con el fin de aumentar o disminuir la amplitud de la onda. El programa muestra además el uso de algunas funciones GDI ya comentadas, así como funciones de teclado como las se van a comentar en el siguiente apartado.
LISTADO 1: Ejemplo de funciones GDI. //----------------------------------------------------------- // EJ1.CPP - Programa de ejemplo de programación bajo Win95. // (c) 1998, Santiago Romero AKA NoP / Compiler, //----------------------------------------------------------- #include <windows.h> #include <math.h> //--- Declaración de funciones del programa ------------------ //--- Declaración de variables del programa ------------------ int amplitud = 60; //=== Función principal WinMain()============================== int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { HWND hwnd; MSG msg; WNDCLASSEX wcx; wcx.cbSize = sizeof( WNDCLASSEX ); (etc.) // estilo WS_CAPTION: Sin botones ni resize. if( !RegisterClassEx( &wcx ) ) return( FALSE ); hwnd = CreateWindowEx( WS_EX_OVERLAPPEDWINDOW, WindowName, WindowTitle, WS_CAPTION, CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, NULL, NULL, hInstance, NULL); (etc.) BucleDeMensajesWhile(); return( msg.wParam ); } //=== Función del procedimiento de ventana WndProc() ===== LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) { HDC hdc; PAINTSTRUCT ps; RECT rect; int loop, altura; double radianes; switch( message ) { case WM_KEYDOWN: switch( wParam ) { case VK_ESCAPE: case VK_F12: DestroyWindow( hwnd ); break; case VK_UP: if( amplitud < 80 ) amplitud+=2; GetClientRect( hwnd, &rect ); InvalidateRect(hwnd, &rect, TRUE); break; case VK_DOWN: if( amplitud > 10 ) amplitud-=2; GetClientRect( hwnd, &rect ); InvalidateRect(hwnd, &rect, TRUE); break; }; return(0); case WM_PAINT: hdc = BeginPaint( hwnd, &ps ); GetClientRect( hwnd, &rect ); // dibujamos el eje horizontal MoveToEx(hdc, 20, 100, NULL); LineTo( hdc, 370, 100 ); // dibujo de la función A * sin(x) for( loop=0, radianes=0.0; loop<350; loop++ ) { radianes = radianes + 0.04f; altura = (int) (((double) amplitud)*sin(radianes)); SetPixel(hdc, 20+loop, 100+altura, RGB(0,0,200)); } // mensajes de información TextOut( hdc, 8, 215, "ARRIBA/ABAJO: Modificar amplitud.",33); TextOut( hdc, 8, 235, "ESCAPE/F12 : Salir del programa.",33); EndPaint( hwnd, &ps ); break; // mensaje producido al cerrar la ventana case WM_DESTROY: PostQuitMessage( 0 ); break; // resto de mensajes, dar una respuesta estándar. default: return( DefWindowProc( hwnd, message, wParam, lParam ) ); } return(0); }
MENSAJE WM_KEYDOWN. Descripciones: --------------------------------------------------------- (int) wParam; Código de la tecla virtual. LParam; Datos extra de la tecla pulsada: Especifica el número de repeticiones, scan code, etc: Bits 0-15: Nº de repeticiones generadas. Bits 16-23: Scancode Bit 24: Especifica si es extendida (1/0). Bit 30: Especifica el estado anterior de la tecla antes del mensaje actual (1=pulsada anteriormente, 0=no pulsada). Se debe devolver cero tras procesar este mensaje (return(0)).El parámetro wParam, como se ha comentado, contiene el valor virtual de la tecla pulsada. Las asignaciones de las distintas teclas virtuales pueden verse en la tabla 1. Un ejemplo del uso de este mensaje está también incluido dentro del listado 1. Por otra parte, el mensaje WM_KEYUP (con los mismos parámetros que WM_KEYDOWN) nos indicará el momento en que una tecla que estaba siendo pulsada ha sido liberada.
TABLA 1: Teclas virtuales:Teclas numéricas del NUMPAD IDENTIFICADORES DE TECLAS VIRTUALES MÁS USUALES: ------------------------------------------------------------------ VK_F1 a VK_F12 Teclas F1 a F2 VK_ESCAPE Tecla escape VK_TAB Tecla tabulador VK_CAPITAL Tecla BlockMays VK_SHIFT Tecla mayúsculas VK_CONTROL Tecla control VK_SPACE Tecla espacio VK_BACK Tecla borrar VK_RETURN Tecla intro VK_PAUSE, VK_CANCEL, VK_INSERT, VK_DELETE, VK_HOME, (las 6) VK_END Teclas del bloque sobre los cursores. VK_LEFT, VK_UP, VK_RIGHT, (las 4) VK_DOWN Teclas de cursor (izq., arriba, der., abajo) VK_NUMLOCK, VK_DIVIDE, VK_MULTIPLY, VK_SUBTRACT, VK_ADD, VK_NUMPAD0 y (todas) VK_NUMPAD9 Teclas numéricas del NUMPADPara el control de las teclas con caracteres directamente en formato ASCII (por lo tanto más fácilmente manipulables para determinados objetivos) disponemos del mensaje WM_CHAR, que nos devuelve en wParam la tecla pulsada, (traducida en el bucle de mensajes gracias a TranslateMessage()):
WM_CHAR: wParam = código del carácter. LParam = flags extra (como WM_KEYDOWN).La tecla pulsada (wParam) se tratará después como cualquier pulsación obtenida por getch() bajo MSDOS, pudiendo utilizarse para discernir diferentes acciones en nuestro programa (incluyendo secuencias de escape).
case WM_CHAR: tecla = wParam; if( tecla == '\n' ) { ... } else if( tecla == 'A' ) { ... } etc...Finalmente, si queremos comprobar el estado de las teclas en el bucle principal de nuestro programa (que debe estar situado dentro de la función WinMain, en el bucle de mensajes), podemos ignorar los mensajes y comprobar directamente el estado de las teclas mediante las funciones:
GetKeyState( Tecla_Virtual ); GetAsyncKeyState( Tecla_Virtual );Estas funciones devuelve un valor cuyo bit más significativo indica si la tecla especificada como virtual (un identificador VK_xxx o un código ASCII a-z) está siendo pulsada o no lo está. La diferencia entre ambas radica en que la primera debe ser llamada en respuesta a un mensaje de teclado, mientras que la segunda puede ser llamada en cualquier punto de nuestro programa (el objetivo que nos habíamos planteado).
if( GetAsyncKeyState( VK_RIGHT ) & 0x8000 ) { ... } // cursor derecho pulsadoLa segunda función (GetAsyncKeyState()) indica además si la tecla se ha pulsado desde la última vez que se llamó a la función, utilizando para ello el bit menos significativo (bit 0).
MENSAJE SIGNIFICADO -------------------------------------------------------------------------- WM_MOUSEMOVE El ratón se mueve sobre el área cliente de la ventana. No se recibe uno de estos mensajes por cada pixel, sino que depende del hardware del ratón. WM_LBUTTONDOWN Botón izquierdo (L=left) del ratón pulsado. WM_MBUTTONDOWN Botón central (M=medium) del ratón pulsado. WM_RBUTTONDOWN Botón derecho (R=right) del ratón pulsado. WM_LBUTTONUP Botón izquierdo del ratón soltado. WM_MBUTTONUP Botón central del ratón soltado. WM_RBUTTONUP Botón derecho del ratón soltado. WM_LBUTTONDBLCLK Doble click con el botón izquierdo (ver nota). WM_MBUTTONDBLCLK Doble click con el botón central (ver nota). WM_RBUTTONDBLCLK Doble click con el botón derecho. (NOTA: se recibirán estos 3 mensajes si hemos configurado la ventana para aceptar doble click). Para todos estos mensajes: lParam: Posición del ratón LOWORD(lParam) = Coord. X del ratón. HIWORD(alParam) = Coord. Y del ratón. wParam: Estado de los botones y teclas CTRL y SHIFT wParam & MK_LBUTTON = 1/0 botón izquierdo pulsado. wParam & MK_MBUTTON = 1/0 botón central pulsado. wParam & MK_RBUTTON = 1/0 botón derecho pulsado. wParam & MK_SHIFT = 1/0 shift pulsado. wParam & MK_CONTROL = 1/0 control pulsado.Si queremos obtener la posición del cursor del ratón en cualquier punto del programa sin esperar un mensaje podemos utilizar la función GetCursorPos( POINT posicion );, que nos devuelve dicha posición en pixels en una estructura tipo point (campos .x, .y). De forma análoga, podemos cambiar la posición del cursor en cualquier instante mediante SetCursorPos(x, y), y mostrar u ocultar el puntero del ratón mediante ShowCursor( TRUE/FALSE); .
Aparte de los mensajes mencionados anteriormente, no está de más comprobar si el ratón está presente en el sistema, mediante GetSystemMetrics( parámetro_a_comprobar ):
if( !GetSystemMetrics( SM_MOUSEPRESENT ) ) // ¡¡error!!Otras funciones interesantes a examinar en las referencias del compilador son SetCapture( hwnd ) y ReleaseCapture(), que hacen que todos los movimientos del ratón sean capturados a la ventana hwnd (aunque esté fuera de la misma).
El mensaje WM_PAINT es ya harto conocido por todos nosotros, y lo recibimos cuando parte del área cliente de nuestra ventana resulta invalidada. Al ocurrir esto, mediante BeginPaint() y EndPaint() se valida este área, aunque también podemos utilizar la función de la API ValidateRect( hwnd, RECT rect ); tras dibujar en el área especificada como inválida. En todos los ejemplos hasta ahora hemos utilizado GetClientRect( hwnd, RECT &rect) para obtener un RECT que indique las coordenadas que atañen al área cliente de nuestra ventana.
Una estructura de tipo RECT (que podemos utilizar como cualquier variable en nuestros programas) está definida como sigue, e indica las coordenadas (x1,y1), (x2,y2) de un rectángulo (en pixels):
typedef struct tagRECT { int left; int top; int right; int bottom; } RECT;Nosotros podemos generar mensajes WM_PAINT desde el bucle WinMain() (es decir, desde el bucle de nuestro programa o juego) para obligar a nuestro programa a redibujar toda o parte de la pantalla, utilizando UpdateWindow( hwnd ), o la función InvalidateRect(hwnd, rectangulo, fErase);, donde fErase es un flag que indica si Windows debe o no borrar el rectángulo especificado antes de generar un WM_PAINT (borrar la ventana si el rectángulo es todo el área cliente).
El mensaje WM_ACTIVATEAPP es recibido por una ventana cuando va a recibir o perder el foco (es decir, cuando cambiamos a una ventana desde otra, pulsando ALT+TAB, o mediante el uso del teclado o del ratón), dándonos la oportunidad de quedarnos en background sin trabajar hasta que el usuario regrese. Para ello, al recibir este mensaje tendremos en wParam un valor que será TRUE (distinto de 0) si nuestra aplicación está siendo activada, o FALSE (=0) si está siendo desactivada. Esto nos permitirá realizar cosas como:
// función de proc. de ventana case WM_ACTIVATEAPP: activo = (BOOL) wParam; // bucle principal: while( 1 ) if( activo == TRUE ) DibujarEnPantalla();Finalizando ya con el tema de los mensajes básicos, el mensaje WM_SIZE nos informa de que ha habido un cambio de tamaño de la ventana, cuyo ancho y alto podemos obtener en el word bajo y alto, respectivamente, del parámetro lParam. El parámetro wParam nos indica además de qué manera ha sido modificado este tamaño (SIZE_MINIMIZED, SIZE_MAXIMIZED y SIZE_RESTORED son los valores que contendrá si ha sido minimizada, maximizada o restaurada, respectivamente).
La utilización de timers en los programas de Windows está muy extendida, pues el programador cuenta así con la posibilidad de ejecutar una tarea específica un nº determinado de veces por segundo, como por ejemplo actualizar un fotograma de un juego, o controlar una serie de parámetros que van en función del tiempo (como trayectorias de sprites), o cualquier otra aplicación imaginable.
Para poder realizar este tipo de tareas, Windows nos provee de las funciones SetTimer() y KillTimer(), que se encargarán de crear y destruir, respectivamente, un temporizador que nos enviará mensajes WM_TIMER con la frecuencia deseada a nuestra función de procesado de mensajes.
SetTimer( hwnd, TimerID, TimeOut, Pfunction );
Mediante esta función podemos autoenviarnos mensajes WM_TIMER a la ventana
<hwnd> especificada, cada <TimeOut> milisegundos. El parámetro
<TimerID> significa un identificador de temporizador (de 1 en adelante),
de manera que podamos crear más de un temporizador para nuestras aplicaciones.
El parámetro Pfunction es un puntero a función de manera que el timer
ejecutará dicha función cuando pase el tiempo especificado, o NULL si
simplemente queremos recibir mensajes WM_TIMER en el WndProc().
KillTimer( hwnd, TimerID );
Esta función elimina el timer definido por TimerID (1...x) asociado a la
ventana hwnd.
Para utilizar un temporizador basta con inicializarlo 1 vez (por ejemplo, durante un mensaje WM_CREATE), gestionar todos los mensajes WM_TIMER que se reciban y destruirlo al salir del programa (por ejemplo, en WM_DESTROY), tal y como hace el siguiente código y el programa de ejemplo del listado 2, que implementa un sencillo cronómetro digital para Windows.
a). Creación del timer. A modo de ejemplo se va a crear un timer que genere un mensaje WM_TIMER cada segundo (1000 milisegundos = 1 segundo). A este timer (por ser el primero) se le da el nº de id. 1:
// función de procesado de mensajes case WM_CREATE: SetTimer( hwnd, 1, 1000, NULL );b). Destrucción del timer. Utilizando KillTimer():
// función de procesado de mensajes case WM_DESTROY: KillTimer( hwnd, 1 );c). Gestión de los mensajes WM_TIMER. Dentro de ellos realizaremos la acción que se desea temporizar o eventualizar:
// función de procesado de mensajes case WM_TIMER: MoverSprites(); ActualizarVariables(); FuncionesGDI(); etc();Al recibir un mensaje WM_TIMER, el parámetro wParam contiene el identificador del timer que ha generado el mensaje (si tenemos más de 1).
LISTADO 2: Crono mediante timers. //----------------------------------------------------------- // EJ2.CPP - Programa de ejemplo de programación bajo Win95. // (c) 1998, Santiago Romero AKA NoP / Compiler, //----------------------------------------------------------- #include <windows.h> //--- Declaración de funciones del programa ------------------ //--- Declaración de variables del programa ------------------ int minutos, segundos, horas; //=== Función principal WinMain()============================== int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { HWND hwnd; MSG msg; WNDCLASSEX wcx; wcx.cbSize = sizeof( WNDCLASSEX ); (etc.) // estilo WS_CAPTION: Sin botones ni resize. if( !RegisterClassEx( &wcx ) ) return( FALSE ); hwnd = CreateWindowEx( WS_EX_OVERLAPPEDWINDOW, WindowName, WindowTitle, WS_CAPTION, CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, NULL, NULL, hInstance, NULL); (etc.) BucleDeMensajesWhile(); return( msg.wParam ); } //=== Función del procedimiento de ventana WndProc()===== LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) { HDC hdc; PAINTSTRUCT ps; RECT rect; char cadena[40]; switch( message ) { case WM_CREATE: minutos=segundos=horas=0; SetTimer( hwnd, 1, 1000, NULL ); break; case WM_KEYDOWN: if( wParam == VK_ESCAPE ) DestroyWindow( hwnd ); return(0); case WM_TIMER: segundos++; if( segundos > 60 ) { minutos++; segundos=0; } if( minutos > 60 ) { horas++; minutos = 0; } if( horas > 24 ) horas = 0; GetClientRect( hwnd, &rect ); InvalidateRect( hwnd, &rect, TRUE ); break; case WM_PAINT: hdc = BeginPaint( hwnd, &ps ); GetClientRect( hwnd, &rect ); wsprintf(cadena, "%02d:%02d:%02d", horas, minutos, segundos); DrawText( hdc, cadena, -1, &rect, DT_SINGLELINE | DT_CENTER |DT_VCENTER ); EndPaint( hwnd, &ps ); break; case WM_DESTROY: KillTimer( hwnd, 1 ); PostQuitMessage( 0 ); break; default: return( DefWindowProc( hwnd, message, wParam, lParam ) ); } return(0); }Mediante la utilización de estas 2 funciones es posible temporizar cualquier tipo de evento que no requiera mucha precisión (la frecuencia máxima de este timer es de aprox. 18.2 mensajes por segundo, de manera que no es un efectivo controlador de fps o fotogramas por segundo), tal y como se hace en el listado 2. Si se desea más precisión de temporización hemos de irnos a los timers multimedia y funciones como QueryPerformanceCounter(), pero eso escapa al objetivo de este mes.
El ejemplo 2 puede convertirse fácilmente en un reloj digital leyendo la hora actual en cada mensaje WM_TIMER recibido, o incluso en uno analógico (con sus manecillas) utilizando la función LineTo() del GDI junto con las funciones sin() y cos() para obtener la posición final de la línea desde el centro de la esfera del reloj.
Pulse aquí para bajarse los ejemplos y listados del artículo (41 Kb).
Santiago Romero