Principal | Gráficos 3D | Gráficos 2D | Fractales | Math | Códigos | Tutoriales | Links

INTRODUCCION A LA PROGRAMACION BAJO WINDOWS (parte 1)

El entorno Windows se ha convertido en el estándar de ejecución de juegos y aplicaciones, y el MS-DOS puede considerarse prácticamente muerto, de manera que en este artículo se va a tratar de ayudar a los programadores de MS-DOS a ingresar de una manera sencilla en el mundode la programacion bajo Windows.

MS-DOS 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 caracteristicas nos resulta dificil aceptar que está acabado. Las nuevas tecnologías de 32bits han desplazado por completo a nuestro querido MS-DOS, 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 esta serie de tutoriales se va a tratar de conseguir que los programadores de MS-DOS (C/C++/Pascal) que deseen aprender a programar en el entorno Windows lo hagan de una manera sencilla. Por supuesto, la orientación de tutoriales aparte de introductoria tratará de derivar hacia el objetivo de poder abordar la programación bajo OpenGL. La finalidad es, introducir en la filosofía de la programación de juegos y aplicaciones gráficas bajo Windows en C y C++, con la ayuda del compilador Visual C/C++.


COMPILACION BAJO WINDOWS

Lo primero que se va a comentar es la manera de compilar un programa bajo Windows. 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


Programa de Windows de ejemplo

Incluir en Project Settings del VisualC++ la libreria winmm.lib
esto nos permite ejecutar el sonido
*/

#include <windows.h>


int WINAPI WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine, int nCmdShow )
{
 PlaySound( "shotgun.wav", NULL, SND_FILENAME | SND_ASYNC);
 MessageBox( NULL, "Primer ejemplo de programación WINDOWS",
		    "Shotgun !!!", 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 el compilador de Microsoft Visual C++ 6.0.

Abrimos el menú File, y seleccionamos la opción New, en Project name le asignamos un nombre al proyecto, en Location seleccionamos un directorio donde se van a guardar los archivos del proyecto, en Projects seleccionamos el tipo de proyecto en este caso Win32 Application.

Seleccionamos el tipo de aplicacion que deseamos crear, en este caso An empty project, una aplicacion vacia

Tras esto se creará el entorno de trabajo y podremos ver ya la ventana de Class View y FileView 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 C++ source File, en la seccion File name ponemos el nombre del archivo que vamos a crear.

Escribimos el código del programa del Listado 1 (podemos copiar y pegar) y lo guardamos ..

Por otra parte, necesitaremos incluir las librerías que vamos a utilizar en nuestro código. En nuestro caso hemos usado a propósito la función PlaySound de la librería multimedia para ilustrar este proceso, vamos al menu Project > Settings... y en Object/library modules agregamos winmm.lib.

El proceso de compilación/linkado y ejecución del programa se puede realizar mediante el menú Build, Build Listado_1.exe o F7, y para ejecutar el programas Execute Listado_1.exe o Ctrl+F5.


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.
*/

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


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 tutorial 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 nos 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.


Códigos fuentes de los dos ejemplos de este tutorial

Los ejemplos los compile con el VisualC++ 6.0

Listado 1 ( incluye shotgun.wav para la prueba del sonido).
Listado 2

valcoey@hotmail.com

Ramiro Alcocer, 2001

Principal | Gráficos 3D | Gráficos 2D | Fractales | Math | Códigos | Tutoriales | Links