INTRODUCCIÓN A LA PROGRAMACIÓN WINDOWS 95/NT

Artículo 3: GDI, MENSAJES Y TIMERS

Autor: (c) Santiago Romero.
Revista: Programación Actual (Prensa Técnica) nº 17, Agosto-1998


La interface gráfica de Windows (GDI), los temporizadores y la recepción de mensajes como los generados por el teclado y mouse son las tareas pendientes más importantes que se van a resolver en el artículo de este mes.

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.


PRINCIPIOS BÁSICOS DEL GDI

El GDI (Graphics Device Interface) es el dispositivo de Windows que hace de mediador entre nuestra aplicación y el dispositivo gráfico, realizando las funciones gráficas que se le demanden si éstas están relacionadas con nuestra ventana. Un concepto importante es el área cliente de una ventana, que se define como toda aquella zona de la misma en que podemos trabajar, excluyendo el menú, los bordes y la barra de título.

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.


FUNCIONES DEL GDI

Una vez obtenido el handle al contexto de dispositivo del área cliente de nuestra ventana, podemos usar sobre ella las diferentes (y variadas) funciones de que dispone el GDI, basten como ejemplo las siguientes:

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);
}


MENSAJES MÁS COMUNES

A continuación vamos a ver algunos de los mensajes más comunes y la forma de procesarlos. Estos mensajes nos van a permitir ampliar en mucho nuestra desenvoltura en Windows, así como realizar ejemplos más complejos.


MENSAJES DE TECLADO

Cuando se pulsa una tecla que no es de sistema (es decir, sin usar la tecla ALT) teniendo el focus (foco) en nuestra ventana, nuestra aplicación recibe un mensaje WM_KEYDOWN que nos va a permitir averiguar qué tecla ha sido pulsada, así como saber cuantas pulsaciones consecutivas se ha mantenido en ese estado. Para ello, en nuestra función de procedimiento de ventana, el parámetro wParam recibirá el indicador de la tecla virtual pulsada mientras que lParam obtendrá unos flags o indicadores especiales sobre dicha pulsación:

 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 NUMPAD

Para 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 pulsado

La 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).


MENSAJES DE RATÓN

El ratón es uno de los dispositivos más útiles de los que nos provee Windows, de modo que dispone de (como mínimo) 10 mensajes de gran utilidad. En todos ellos se nos informa de la posición (x,y) del ratón al generarse en mensaje en la palabra baja y alta respectivamente del parámetro lParam, que podremos extraer mediante las macros LOWORD() e HIWORD() comentadas el mes pasado.

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).


OTROS MENSAJES COMUNES

Hay otra serie de mensajes también importantes para controlar procesos comunes en juegos y aplicaciones. Algunos de ellos ya los hemos visto, como WM_CREATE, WM_QUIT y WM_DESTROY. El primero resulta muy útil para inicializar variables, rutinas, tomar los datos sobre la fuente de la ventana, y, como veremos, lanzar temporizadores. Los 2 últimos se ejecutan cuando el usuario sale de la aplicación, y nos van a permitir llamar a funciones de “limpieza” de nuestro programa (liberar memoria, etc.).

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).


TEMPORIZADORES: EL MENSAJE WM_TIMER

Para finalizar la presente entrega, se va a comentar la posibilidad de utilización de temporizadores o timers en nuestros programas, así como las posibles aplicaciones que esto pueda presentar.

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.


EN RESUMEN

Mediante lo visto hoy se puede obtener cualquier clase de input por parte del usuario (pulsaciones de teclado o ratón), así como trazar sencillos gráficos en pantalla (usando el GDI), e incluso temporizar eventos en nuestro programa Windows, de manera que es posible proponerse diversos ejercicios un sencillo programa de dibujo (mediante el ratón y SetPixel/LineTo), un reloj, etc. Ni que decir tiene que esa es la mejor manera de aprender a manejarse en el mundo Windows, enfrentándose a él.


LA PRÓXIMA ENTREGA

El mes que viene finalizaremos nuestro curso de introducción a Windows 95 y NT explicando cómo generar y manipular procesos multitarea y cómo organizar el bucle principal de nuestras aplicaciones y juegos (para que hagan cosas más complejas aparte de responder mensajes), ilustrado con un sencillo ejemplo gráfico. Hasta entonces, lo mejor es practicar lo aprendido mediante la realización de algún programa o aplicación sencilla.

Pulse aquí para bajarse los ejemplos y listados del artículo (41 Kb).

Santiago Romero


Volver a la tabla de contenidos.