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

Artículo 1: INTRODUCCIÓN AL MUNDO WINDOWS

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


El entorno Windows se ha convertido en el estándar de ejecución de juegos y aplicaciones, y el mercado de MSDOS puede considerarse prácticamente muerto, de manera que en esta serie de artículos se va a tratar de ayudar a los programadores de MSDOS a ingresar de una manera sencilla en el mundo Windows.

MSDOS fue la estrella indiscutible de los sistemas operativos (en cuanto a número de usuarios y a facilidad de utilización), y a todos aquellos que dedicamos parte de nuestra vida a aprender y dominar todas sus funcionalidades y características nos resulta difícil aceptar que está acabado. Las nuevas tecnologías de 32bits han desplazado por completo a nuestro querido MSDOS, y si somos o pretendemos ser desarrolladores de software no nos queda otro remedio que comenzar a implementar nuestras ideas en este sistema operativo.

Mediante este mini-curso de 3/4 artículos se va a tratar de conseguir que los programadores de MSDOS (C/C++/Pascal) que deseen aprender a programar en el entorno Windows lo hagan de una manera sencilla. Por supuesto, la orientación del curso aparte de introductoria tratará de derivar hacia el objetivo de poder abordar (en otra sección de la revista) la programación bajo DirectX y Windows. La finalidad es, pues, introducir al lector en la filosofía de la programación de juegos y aplicaciones gráficas bajo Windows95 y NT en C y C++, con la ayuda del compilador Visual C/C++. Se ha elegido un curso de pocos capítulos (3 ó 4), pero con mucha información, para que el lector no tenga que esperar muchos meses para comenzar su andadura en Windows.


WINDOWS, DIRECTX, Y EL DESARROLLO

Cualquiera que haya mirado un programa bajo Windows (por simple que sea, como el programa del listado 2), es posible que se haya sentido intimidado por su estructura y la cantidad de llamadas a funciones (y parámetros) que incluye. Pero programar en Windows no es tan difícil como dejan entrever estos programas, por diferentes razones:

Programar bajo Windows requiere aprender una serie de funciones necesarias para inicializar la ventana principal (u otra ventana hija de la anterior). Dichas funciones (y la cantidad de parámetros que requieren) hacen al programa engordar en cuanto a líneas de código, de manera que cualquier programa básico requiere su utilización, es decir: tras el aprendizaje de esas funciones dispondremos de una plantilla para crear el resto de programas (prácticamente todos llevan similar código de inicialización), y a partir de ese momento la adición de líneas de código ya corresponde a las distintas acciones que queramos llevar a cabo sobre esta ventana.

Una vez dominados los conceptos básicos de Windows, cabe señalar que DirectX (sobre todo DirectDraw) hace la programación de Windows muy similar a la programación bajo DOS, como ya veremos, de manera que lo aprendido aquí será muy útil cuando se aborde la programación mediante DirectX. Por otra parte, existen multitud de Wrappers (librerías con una clase C++ para encapsular DirectX) que proporcionan todas las funciones necesarias para utilizar DX, por lo que incluso no es necesario conocer DX para realizar programas bajo Windows95. En un próximo artículo, se incluirán varios de dichos Wrappers (gratuitos) en el CD que acompaña a la revista (tales como CDX, ACX, EasyX, DirectXEasy, etc.).

Finalmente, comentar que Windows y DirectX nos van a proporcionar el control de todos los tipos de tarjeta de vídeo y audio sin preocuparnos de los modelos o características, es decir, podremos finalizar el dedicarnos a “aprender a programar tarjetas” y comenzar a “aprender a programar programas”, lo cual da más beneficios y elimina problemas en otros equipos.


COMPILACION BAJO WINDOWS

Lo primero que se va a comentar es la manera de compilar un programa bajo Windows 95 o NT. Para ello tomaremos como ejemplo el listado 1, que simplemente muestra un DialogBox (caja de diálogo informativo con botones de acciones) y suena un fichero WAV utilizando funciones de la API (Application Programmable Interface, conjunto de funciones disponibles por Windows para el programador).

 LISTADO 1: Sencillo programa de Windows:

#include <windows.h>

int WINAPI WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
			  LPSTR lpCmdLine, int nCmdShow )
{

   PlaySound( "ejemplo.wav", NULL, SND_FILENAME | SND_ASYNC);
   MessageBox( NULL, "Primer ejemplo de programación WINDOWS",
		   "1998 Compiler Software", MB_OK|MB_ICONASTERISK) ;
   return(0);
}

Para nuestro objetivo se va a describir el proceso de creación y compilación del listado 1 en los compiladores Microsoft Visual C++ 4.x y 5.0 (los mejores en cuanto a generación de código bajo Windows 95). Como ya se dispuso de una sección sobre ello en la revista (Curso de Visual C++), se comentarán sólo las acciones más útiles para la creación de nuestros programas.

1. Abrimos el menú FILE, y seleccionamos la opción NEW. De la lista de NUEVOS FICHEROS a crear (new file, new bitmap, etc.), seleccionamos NEW PROJECT WORKSPACE (nuevo espacio de trabajo), que significa crear un directorio y fichero para almacenar todos los datos de nuestro nuevo proyecto (cada proyecto puede tener opciones individuales que se almacenan en el fichero de proyecto).

2. Tras especificar que queremos abrir un nuevo proyecto, Visual C++ nos preguntará qué tipo de proyecto deseamos abrir (Aplicación MFC, Aplicación de Consola (es decir, de texto pero bajo Windows), etc.). Seleccionamos APPLICATION (Win32), introducimos el nombre del proyecto en la celda de texto Name, pulsando Create.

3. Tras esto se creará el entorno de trabajo y podremos ver ya la ventana de CLASES y FICHEROS que aparece nuestro proyecto (desde esa ventana podemos movernos a cualquier fichero o clase del proyecto mediante un doble click). Es el momento de crear el fichero .CPP que contendrá el código de nuestro programa. Para ello vamos de nuevo al menú FILE->NEW, pero esta vez seleccionamos NEW TEXT FILE.

4. Escribimos el código del programa (ver listado 1) y salvamos el fichero de texto como nombre.CPP. En este fichero de texto, aparte de nuestro código y de nuestros ficheros de cabecera, debe figurar la inclusión del archivo <windows.h>.

5. A continuación es el momento de incluir en el proyecto otros ficheros .CPP relacionados con él (por ejemplo, si disponemos de una clase OBJETO.HPP y OBJETO.CPP, se incluirá el primero con #include, y el segundo mediante el menú INSERT, opción FILES INTO PROJECT. En la ventana de clases y ficheros podremos ver su aparición inmediata.

6. Por otra parte, necesitaremos incluir las librerías que hayamos utilizado en nuestro código (por ejemplo, ddraw.lib si utilizáramos DirectDraw). En nuestro caso hemos usado a propósito la función PlaySound de la librería multimedia para ilustrar este proceso, de manera que en INSERT -> FILES INTO PROJECT, cambiamos la celdilla de tipo de archivo a *.LIB, vamos a \MSDEV\LIB (o cualquiera que sea el path del compilador), e incluimos WINMM.LIB (librería de funciones multimedia).

7. El proceso de compilación/linkado y ejecución del programa se puede realizar mediante el menú BUILD (opciones BUILD xxx.exe y EXECUTE xxx.exe) o mediante las teclas F7 y CTRL+F5 respectivamente. El resultado de la compilación y linkado serán 2 subdirectorios DEBUG y RELEASE que contendrán la versión de DEBUG y FINAL de nuestro programa. Si no se desea la creación de la versión debug (o de la release), puede eliminarse en el menú BUILD -> CONFIGURATIONS -> REMOVE.


¿MFC o NO MFC?

Las MFC (Microsoft Foundation Classes) son un conjunto de clases creadas por Microsoft destinadas a hacer más sencilla la programación bajo el entorno Windows. Contienen funciones para multitud de objetos del mundo Windows (bitmaps, ventanas, comboboxes, etc.). En teoría realizar un programa bajo Windows utilizando las MFC es muy sencillo, pero la verdad es que estas clases no gozan de muy buena popularidad entre los desarrolladores de juegos, aunque sí entre los de aplicaciones (realmente la creación es más rápida y sencilla).

Como nuestro curso tiene como objetivo introducir la programación Windows no se van a utilizar estas librerías, pero para usarlas basta con crear una APPLICATION MFC en los pasos 1 y 2 del proceso de compilación que acabamos de ver. Su principal desventaja es que además de las funciones de Windows, se necesitaría estudiar las funciones de las clases MFC.


FILOSOFIA DE WINDOWS

Muchos de nosotros hemos caído en el error de pensar que los programas que se ejecutan bajo Windows lo hacen mucho más lentos de lo que lo harían bajo DOS. Esa conclusión la hemos sacado a partir de dos premisas, una cierta y la otra falsa, como vamos a ver a continuación.

Lo que es totalmente obvio es que la mayoría de PC’s disponen tan sólo de un procesador o CPU, y que no es lo mismo dedicar todo el tiempo de la CPU a un programa (MSDOS, monotarea) que a varios (WINDOWS, multitarea). Esa es nuestra primera premisa, la verdadera.

La segunda premisa (la errónea) constituye el pensar que si ejecutamos 2 programas de MSDOS bajo Windows, cada uno funcionará al menos a la mitad de la velocidad del original. Windows (o los sistemas multitarea en general) no basa el multiproceso en dar una porción de tiempo a cada programa (timeslicing); si esto fuera así, realmente al ejecutar 2 programas cada uno iría a la mitad de la velocidad del original. La clave de Windows consiste en que es un sistema basado en mensajes.

Al crear un programa de Windows, como siempre, habremos de inicializar las ventanas de la aplicación (como ya veremos), pero la ejecución del programa no es lineal (como en MSDOS, donde resulta muy fácil seguir el flujo del programa desde el punto de entrada (main()) hasta la salida (final de main()). En Windows también disponemos de un punto de entrada al programa (WinMain()), pero tras ejecutarse el código de inicialización de nuestra aplicación, ésta no sigue un flujo lineal (línea por línea esperando a que Windows le asigne su porción de tiempo), sino que se queda esperando, recogiendo mensajes de Windows y contestando a éstos de manera que la aplicación haga lo deseado.

A título de ejemplo está el programa del listado 2. Este programa tiene como punto de entrada la función WinMain, donde (como explicaremos a continuación) se inicializa la ventana principal y se entra al bucle de mensajes (la porción de código donde vemos la función GetMessage()): es decir, la aplicación se dedica a recibir mensajes de Windows.

  LISTADO 2: Programa Windows de ejemplo.

//------------------------------------------------------------
// TEST2.CPP  -  Programa de ejemplo bajo Windows95.
// (c) 1998, Santiago Romero AKA NoP / Compiler.
//------------------------------------------------------------
#include <windows.h>

//--- Declaración de funciones del programa ------------------
int WINAPI WinMain( HINSTANCE, HINSTANCE, LPSTR, int );
LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM );


//--- Declaración de variables del programa ------------------
char WindowName[]  = "Ventana de Windows";
char WindowTitle[] = "¡Hola, Mundo!";


//=== Función principal WinMain()============================
int WINAPI WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
			   LPSTR lpCmdLine, int nCmdShow )
{
  HWND hwnd;
  MSG msg;
  WNDCLASSEX wcx;

// Definimos la estructura de clase de ventana (campos):
  wcx.cbSize = sizeof( WNDCLASSEX );      // tamaño de la estruct.
  wcx.style = CS_HREDRAW | CS_VREDRAW;    // valores más usuales
  wcx.lpfnWndProc = WndProc;              // función de ventana
  wcx.cbClsExtra = 0;
  wcx.cbWndExtra = 0;                     // informaciones extra
  wcx.hInstance = hInstance;              // instancia actual
	
// icono, cursor, fondo e icono pequeño de la clase de ventana:
  wcx.hIcon = LoadIcon(NULL, IDI_WINLOGO);
  wcx.hCursor = LoadCursor(NULL, IDC_ARROW);
  wcx.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH );
  wcx.hIconSm = LoadIcon(NULL, IDI_WINLOGO);

  wcx.lpszMenuName = NULL;                // nombre del menú
  wcx.lpszClassName = WindowName;         // nombre de la ventana

// Registramos la clase de ventana ya preparada:
  if( !RegisterClassEx( &wcx ) )
	return( FALSE );               // si hay error salir

// Creamos la ventana con CreateWindowEx():
  hwnd = CreateWindowEx(
    WS_EX_OVERLAPPEDWINDOW,          // estilo extendido
    WindowName,                      // nombre de la ventana
    WindowTitle,                     // título de la ventana
    WS_OVERLAPPEDWINDOW,             // estilo de ventana
    CW_USEDEFAULT, CW_USEDEFAULT,    // Posición (x,y) en pantalla
    400, 300,                        // ancho y alto de la ventana
    NULL, NULL,                      // ventana padre e hija+menú
    hInstance,                       // instancia actual
    NULL                             // no hay más información
                       );

// Comprobamos la creación de la ventana:
  if( !hwnd )
     return( FALSE );                 // si hay error, salir	

// Hacemos visible la ventana y la actualizamos:
  ShowWindow( hwnd, nCmdShow );
  UpdateWindow( hwnd );

// Bucle de mensajes, envía los mensajes hacia WndProc
  while( GetMessage( &msg, NULL, 0, 0 ) )
  {
    TranslateMessage( &msg );    // convertimos el mensaje
    DispatchMessage( &msg );     // devolvemos el control a w95
  }

// devolvemos el valor recibido por PostQuitMessage().
  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;

 switch( message )
 {
  // mensaje producido en la creación de la ventana
  case WM_CREATE:     
       break;

  case WM_PAINT:
       hdc = BeginPaint( hwnd, &ps );
       GetClientRect( hwnd, &rect );
       DrawText( hdc, "¡Hola Mundo!", -1, &rect,
                 DT_SINGLELINE | DT_CENTER | DT_VCENTER );
       EndPaint( hwnd, &ps );
       break;
	
  // mensaje producido al cerrar la ventana
  case WM_DESTROY:
       PostQuitMessage( 0 );
       break;

  // resto de mensajes, dar una respuesta estándar.
  // dejamos que el propio windows los responda :
     default:
      return( DefWindowProc( hwnd, message, wParam, lParam ) );
   }

  return(0);
}

Estos mensajes se procesan en una función aparte (que se ejecuta paralela y automáticamente cuando se recibe un mensaje, por lo que no es necesario llamarla explícitamente desde WinMain). Si, por ejemplo (ver función WndProc) se recibe un mensaje WM_DESTROY, quiere decir que el usuario ha pulsado el botón de cierre de la aplicación y hay que realizar las acciones necesarias para cerrar la ventana. Si se recibe un mensaje WM_PAINT, Windows nos está indicando que podemos dibujar/escribir lo que deseemos en nuestra ventana, que es hora de redibujar su contenido pues ha ocurrido algún suceso que ha borrado parte de ella (mover otra ventana sobre su superficie, cambiar su tamaño, etc.).

En resumen, tras inicializar nuestra aplicación deberemos contestar los diferentes mensajes de Windows, de manera que con tan sólo unas 50 líneas de código conseguimos una ventana que se puede mover, modificar su tamaño, cerrar, maximizar, con su título y su (opcional) barra de menús. A partir de ese momento podemos aplicar nuestros (futuros) conocimientos de DirectX para convertir esa ventana en la ventana principal de nuestro juego/aplicación, escribiendo en ella los gráficos o textos deseados, y recogiendo de ella el input del usuario.


EL ESQUELETO BASICO

A continuación vamos a comentar el programa de ejemplo que puede observarse en el listado 2, que imprime la cadena “Hola Mundo” en pantalla. El código tiene un tamaño considerable, es cierto, pero por esa cantidad de líneas hemos obtenido una ventana con un texto centrado, donde esa ventana tiene botones de maximizado, minimizado y cierre, puede moverse y ampliarse, etc. (imaginemos por un momento la cantidad de código que tendríamos que escribir para hacer lo mismo bajo DOS: un sistema gráfico, un sistema de ventanas...).

Lo primero que encontramos en el ejemplo 2 es un comentario (al estilo de C++, con dobles barras), la inclusión de Windows.h, la definición de las funciones que utilizaremos y 2 variables de texto que especificarán el título de la ventana y de la aplicación.

Tras eso aparece la función WinMain, que será el punto de entrada a nuestra aplicación (es decir, al ejecutar el programa con un doble click o desde la línea de comandos se estará invocando esta función). Dentro de la misma tenemos las siguientes declaraciones de variables:


  HWND hwnd;
  MSG msg;
  WNDCLASSEX wcx;

En el próximo número veremos todos los campos que tienen estos tipos de datos y otros también muy frecuentes en Windows, pero por ahora nos basta con saber que los tipos HWND corresponden a handles a ventanas (estamos declarando una estructura que contendrá un identificar de nuestra ventana dentro de todas las existentes en el entorno windows), los MSG son estructuras para contener mensajes de Windows, y WNDCLASSEX nos servirá para decirle a Windows cómo queremos que sea la ventana de nuestra aplicación. Veamos algunos campos de esta estructura y como inicializarlos:


// Definimos la estructura de clase de ventana (campos):
  wcx.style = CS_HREDRAW | CS_VREDRAW;    // valores más usuales
  wcx.lpfnWndProc = WndProc;              // función de ventana
  wcx.hIcon = LoadIcon(NULL, IDI_WINLOGO);
  wcx.hCursor = LoadCursor(NULL, IDC_ARROW);
  wcx.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH );
  wcx.hIconSm = LoadIcon(NULL, IDI_WINLOGO);
  wcx.lpszMenuName = NULL;                // nombre del menú
  wcx.lpszClassName = WindowName;         // nombre de la ventana

Con la anterior porción de código le indicaremos a Windows (más adelante en el código, cuando registremos la ventana) que queremos una ventana con el icono de windows en su barra de título (IDI_WINLOGO), con el cursor de flecha (IDC_ARROW), de fondo blanco (WHITE_BRUSH), sin menú (lpszMenuName=NULL) y con el nombre que le indicamos en el último campo.

Tras rellenar la estructura, registramos la clase de ventana, es decir, le pedimos a Windows que la reconozca:


// Registramos la clase de ventana ya preparada:
  if( !RegisterClassEx( &wcx ) )
	return( FALSE );

Tras eso creamos la ventana propiamente dicha:


// Creamos la ventana con CreateWindowEx():
  hwnd = CreateWindowEx(
    WS_EX_OVERLAPPEDWINDOW,          // estilo extendido
    WindowName,                      // nombre de la ventana
    WindowTitle,                     // título de la ventana
    WS_OVERLAPPEDWINDOW,             // estilo de ventana
    CW_USEDEFAULT, CW_USEDEFAULT,    // Posición (x,y) en pantalla
    400, 300,                        // ancho y alto de la ventana
    NULL, NULL,                      // ventana padre e hija+menú
    hInstance, NULL                  // instancia actual
                    );

Mediante la función CreateWindowEx() le pedimos a Windows una ventana de tipo OVERLAPPED_WINDOW extendida (ya veremos en la próxima entrega los diferentes estilos de ventana que podemos crear), con el nombre y título especificados en el segundo y tercer parámetro. Con el resto de parámetros le pedimos que la coloque en la posición (x,y) por defecto de la pantalla (CW_USEDEFAULT), y que sea de tamaño 400x300 pixels. El resultado devuelto por CreateWindowEx es un handle de ventana, que almacenaremos en la variable hwnd declarada al principio del programa. Es importante chequear los errores (aunque son muy improbables).

Ahora que hemos creado la ventana, la hacemos visible y le decimos a Windows que actualice su contenido.


// Hacemos visible la ventana y la actualizamos:
  ShowWindow( hwnd, nCmdShow );
  UpdateWindow( hwnd );

La función UpdateWindow() lo que hace en realidad es enviar un mensaje WM_PAINT. Ese mensaje es enviado por Windows a nuestros programas para que sepamos cuando hay que redibujar nuestra ventana porque algo ha borrado parte de la misma, o incluso cuando cambiamos su tamaño. El resultado es que necesitamos redibujar su contenido, así que Windows nos envía un mensaje WM_PAINT que recogeremos y procesaremos nosotros en una función especial (función CALLBACK) que comentaremos a continuación. Mediante UpdateWindow() nos autoenviamos un mensaje WM_PAINT de manera que nuestra función procesa-mensajes haga el primer dibujado de la ventana (para que escriba en ella por primera vez).

Lo que sigue es el bucle de mensajes, es decir, un bucle que se dedica a recoger los mensajes enviados por Windows.


// Bucle de mensajes, envía los mensajes hacia WndProc
  while( GetMessage( &msg, NULL, 0, 0 ) )
  {
    TranslateMessage( &msg );    // convertimos el mensaje kbd
    DispatchMessage( &msg );     // lanzamos WndProc
  }

la anterior porción de código recoge los mensajes enviados por Windows y los “traduce” de manera que se los envía a la función CALLBACK que hayamos definido al registrar la ventana. Si volvemos atrás en el código, vemos algo así como:


  wcx.lpfnWndProc = WndProc;              // función de ventana

El campo lpfnWndProc es un puntero (lp) a función (fn). Si examinamos la función WndProc() nos daremos cuenta de que tiene un formato especial: es una función CALLBACK, nuestra procesadora de mensajes.


FUNCIONES CALLBACK - RESPONDIENDO MENSAJES

La función CALLBACK WndProc es una función especial. No la llamamos desde ningún punto de programa ni es el punto de entrada. Esta función simplemente es un “procesador de mensajes”, un procedimiento de ventana. En el bucle de mensajes situado en WinMain, la función DispatchMessage() lo que hace realmente es “llamar” a nuestra función de ventana (WndProc) convirtiendo el mensaje de Windows de manera que es pasado como parámetro a esta función.

Así pues, nuestra aplicación necesita un procedimiento de ventana que responda adecuadamente a los mensajes enviados por Windows. Esta función se define con los siguientes parámetros :

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );

El parámetro hwnd es el handle a nuestra ventana (que utilizaremos al realizar operaciones sobre ella). Message es un entero largo (UINT) que contiene un identificador del mensaje que nos ha sido enviado. Los 2 restantes parámetros (wparam y lparam) son valores de 32 bits que constituyen parámetros extra interesantes para el procesamiento del mensaje.

Como ejemplo, en message podemos recibir un número que nos indique que ha ocurrido una pulsación del ratón dentro del área de nuestra ventana, mientras que wparam y lparam nos informarán de las coordenadas donde fue pulsado, por ejemplo.

Lo primero que hemos de hacer es ver qué mensaje hemos recibido:


  switch( message )
  {
    // mensaje producido en la creación de la ventana
    case WM_CREATE :     
           break;

    case WM_PAINT:
           hdc = BeginPaint( hwnd, &ps );
           GetClientRect( hwnd, &rect );
           DrawText( hdc, "¡Hola Mundo!", -1, &rect,
             DT_SINGLELINE | DT_CENTER | DT_VCENTER );
           EndPaint( hwnd, &ps );
           break;
	
     // mensaje producido al cerrar la ventana
     case WM_DESTROY:
           PostQuitMessage( 0 );
           break;

     // resto de mensajes, dar una respuesta estándar.
     // dejamos que el propio windows los responda :
     default:
       return( DefWindowProc( hwnd, message, wParam, lParam ) );
  }

No necesitamos aprender los identificadores numéricos que corresponden a cada mensaje, para ello disponemos de constantes predefinidas más sencillas de recordar. En el código anterior se tratan diferentes mensajes, como WM_CREATE, WM_DESTROY y WM_PAINT.

El mensaje WM_CREATE es el mensaje que recibimos cuando se crea nuestra ventana. Si queremos que el programa visualice un gráfico o suene un sonido en la creación de su ventana principal, podemos hacerlo al recibir este mensaje. Por contra, el mensaje WM_DESTROY es recibido cuando el usuario decide cerrar (destruir) la ventana, en cuyo caso hemos de indicarle al bucle de mensajes que deje de recibir mensajes y termine la ejecución de la aplicación (mediante PostQuitMessage(0)). En cambio, vemos que WM_PAINT es un mensaje que nuestro programa maneja con un tratamiento mucho más extenso, que comentaremos en el siguiente apartado.

Por supuesto, Windows nos puede enviar muchos más mensajes, como que nuestra aplicación ha pasado a segundo o primer plano (WM_ACTIVATEAPP), que se ha pulsado una tecla (WM_KEYDOWN), etc. Para evitar contestar los mensajes que no tienen especial interés para nosotros podemos darle la respuesta estándar de Windows, es decir, pedirle a Windows que conteste al mensaje por nosotros, mediante DefWindowProc( hwnd, message, wParam, lParam ).

Finalmente, salimos de la función con un return(0);


COMENTARIO DE WM_PAINT EN NUESTRO EJEMPLO

Queda por comentar el código que acompaña al mensaje WM_PAINT. Como ya se ha comentado, el mensaje WM_PAINT indica que ha habido un cambio en la ventana y que tenemos que redibujar su contenido. Es decir, cuando una porción de la ventana es modificada de forma ajena a nuestro programa (es invalidada), recibimos un mensaje WM_PAINT para que la volvamos a validar (redibujar):


  hdc = BeginPaint( hwnd, &ps );
  GetClientRect( hwnd, &rect );
  DrawText( hdc, "¡Hola Mundo!", -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER );
  EndPaint( hwnd, &ps );

La función BeginPaint() requiere que le indiquemos la ventana con la que estamos trabajando, y a cambio no devuelve un hdc (Handle to Device Context, o handle al contexto de dispositivo). Un hdc es un identificador del área gráfica que ocupa nuestra ventana, para poder dibujar en ella con las funciones del GDI (Graphics Device Interface, o interface dispositivo de gráficos) utilizando este handle.

EndPaint() por otra parte, hace que devolvamos a Windows el handle de contexto de dispositivo que nos había “prestado”. Es importante llamar a ambas funciones porque estas validan el área invalidada (modificada) por culpa de la cual estamos recibiendo el mensaje, que continuaremos recibiendo hasta que la validemos (al usarlas se valida y se elimina el mensaje, hasta que recibamos otro que nos indique que otra zona de la ventana ha sido invalidada).

A continuación mediante GetClientRect() se coge en una estructura tipo rect (que como se verá en el próximo número es una estructura que indica 2 puntos que delimitan un rectángulo) las coordenadas inicial y final del rectángulo del área cliente (el área cliente es el área gráfica de la ventana sin bordes ni menú, es decir, la sección blanca de la ventana, si así hemos definido su color).

Mediante DrawText escribimos centrado vertical y horizontalmente la frase “Hola Mundo”, de manera que aparezca en el centro de la ventana.

El resultado de contestar esta manera a WM_PAINT es el siguiente:

1.- Al comenzar el programa, llamamos a UpdateWindow() de manera que se genera un mensaje WM_PAINT y se dibuja el texto por primera vez en nuestra ventana.

2.- Mientras no ocurra nada especial con nuestra ventana, el mensaje seguirá ahí y no se recibirá ningún mensaje WM_PAINT.

3.- Cuando ocurra un suceso que obliga a redibujar el texto (movimiento de la ventana, cambio del tamaño...), Windows nos enviará un mensaje WM_PAINT para que volvamos a trazarlo en pantalla.

El resultado es que el texto aparece siempre en pantalla, y que tan sólo lo redibujamos cuando ocurre un evento, lo cual significa que si ejecutamos un programa al mismo tiempo que éste, no le estaremos robando tiempo de CPU al otro, ya que nuestro programa no será llamado (no recibirá ningún mensaje) hasta que ocurra un cambio en su ventana, es por ello que el programa que ejecutamos paralelamente se ejecuta al 100% de su velocidad y no al 50% como cabría esperar al ejecutar 2 programas simultáneamente.


QUE HA CAMBIADO DESDE MS-DOS

El mundo Windows tiene un estilo que en principio intimida, pero que no debe afrontarse con un afán de aprendizaje total. En vez de esto, debe tomarse con otro punto de vista: lo más importante aquí es comprender el concepto básico (lo que hemos aprendido hoy) de Windows (los mensajes), y después tener una buena referencia a mano: esto significa tener un manual o revista donde poder consultar las funciones de Windows y sus parámetros, acudiendo a ella en vez de memorizar todos los tipos de parámetros y estructuras. Esto es lo que se pretende para el siguiente artículo, donde daremos una referencia para todas las funciones y estructuras que se han comentado hoy. También podría considerarse útil hacerse con un manual de referencia sobre Windows, o lo que es mejor, con el CD MSDN de Microsoft (para desarrolladores), e utilizar intensamente los archivos de ayuda del compilador.


LA PRÓXIMA ENTREGA

En la próxima entrega comentaremos más a fondo los diferentes aspectos de Windows, como la cola de mensajes, los parámetros de las funciones básicas comentadas en el presente artículo, así como otras funciones útiles.

Hasta entonces lo mejor es tratar de practicar en la creación y compilación del programa de ejemplo nº 2, y se aconseja como ejercicio que se reescriba dicho ejemplo de manera que se pueda disponer de una plantilla para el resto de nuestros programas, pues todos los programas Windows poseerán una estructura similar.

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

Santiago Romero


Volver a la tabla de contenidos.