Introduccion a la programacion bajo Windows

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

INTRODUCCION A LA PROGRAMACION BAJO WINDOWS (parte 2)

FUNCIONES, PARAMETROS Y MENSAJES

En el tutorial anterior dejamos en el aire el significado de los parámetros de las funciones del programa de ejemplo, así como el contenido de algunas estructuras utilizadas en el código. A continuación se va a tratar de ver todas estas cosas mas otro sencillo ejemplo que introduzca nuevos conceptos. Por supuesto, hoy vamos a tratar la parte mas tediosa de Windows, su cantidad de funciones, parámetros y tipos de datos, aunque aquí se tratara de simplificar su comprensión todo lo posible.


TIPOS DE DATOS MAS FRECUENTES EN WINDOWS

Los tipos de datos mas frecuentes son:

  • HWND: Es un handle a una ventana, como a la ventana principal.
  • HDC: Es un handle a un contexto de dispositivo (para trazar gráficos mediante el, usando la interfaz de Windows).
  • UINT: Es un typedef de un entero de 32 bits sin signo, muy común para devolver datos.
  • WPARAM/LPARAM: Enteros de 32 bits. Son parámetros de WndProc().
  • BOOL: Es un entero de 32 bits booleano.
  • BYTE: Dato de 8 bits.
  • DWORD: Entero de 32 bits sin signo.
  • LONG: Es un entero de 32 bits.
  • LPSTR, LPCSTR: Un puntero a una cadena de caracteres o a una constante de cadena.
  • HICON, HCURSOR, HBRUSH: Handle a un icono, cursor o brochas.
  • HBITMAP: Referente a un bitmap (grafico).
  • RECT: Se usa para determinar rectángulos dentro de un area grafica. Es una estructura que contiene 4 campos, que son las coordenadas (x1, y1, x2, y2) de un rectángulo.
  • POINT: Estructura que permite identificar puntos. Contiene dos campos que son las coordenadas (x,y) de un punto (en pixels desde la esquina de la ventana (0, 0)).

LAS FUNCIONES REGISTERCLASSEX()

Como vimos en el tutorial anterior, esta función registra una ventana a través de una clase de ventana WNDCLASSEX previamente inicializada:

//definimos la estructura de la ventana
WNDCLASSEX wcx;                         // estructura de la ventana

//determinamos los valores de los campos
wcx.style = CS_HREDRAW | CS_VREDRAW;    // valores más usuales

//registramos la clase de la ventana ya registrada
if( !RegisterClassEx( &wcx ) )
		return( FALSE );                    // en caso de error, salir

La variable que se le pasa a RegisterClassEx() es una estructura de tipo WNDCLASSEX, la cual tiene la siguiente forma en los archivos de cabecera de Windows:

typedef struct _WNDCLASSEX { 
    UINT    cbSize; 
    UINT    style; 
    WNDPROC lpfnWndProc; 
    int     cbClsExtra; 
    int     cbWndExtra; 
    HANDLE  hInstance; 
    HICON   hIcon; 
    HCURSOR hCursor; 
    HBRUSH  hbrBackground; 
    LPCTSTR lpszMenuName; 
    LPCTSTR lpszClassName; 
    HICON   hIconSm; 
} WNDCLASSEX;

LOAICON(), LOADCURSOR(), Y GETSTOCKOBJECT()

A la hora de rellenar los diferentes campos de la estructura WNDCLASSEX se usan las funciones LoadIcon(), LoadCursor() y GetStockObject():

LoadIcon(): Sirve para cargar un icono, devolviendo un handle tipo HICON del mismo. En el código de inicialización lo usamos para cargar un icono del stock de Windows y asignárselo a la ventana que se esta creando. Sus parámetro de llamada son los siguientes:

HICON LoadIcon(
  HINSTANCE hInstance, // handle de la instancia
  LPCTSTR lpIconName   // puntero a la cadena con un nombre
                       // o un identificador de recursos
);

El parámetro lpIconName constituye un identificador de recursos, de los cuales los mas habituales son los que pueden verse a continuacion:

IDI_APPLICATION Default application icon. 
IDI_ASTERISK Same as IDI_INFORMATION. 
IDI_ERROR Hand-shaped icon. 
IDI_EXCLAMATION Same as IDI_WARNING. 
IDI_HAND Same as IDI_ERROR.  
IDI_INFORMATION Asterisk icon. 
IDI_QUESTION Question mark icon. 
IDI_WARNING Exclamation point icon. 
IDI_WINLOGO Windows logo icon

LoadCursor(): Sirve para cargar un cursor para el ratón, devolviendo el handle identificador del mismo. De la misma manera lo usamos en WNDCLASSEX para definir el aspecto del cursor cuando nuestra aplicación se este ejecutando (aunque tambien podemos cambiarlo en cualquier momento):

HCURSOR LoadCursor(
  HINSTANCE hInstance,  // handle de la instancia
  LPCTSTR lpCursorName  // puntero a la cadena con un nombre
);

los valores mas habituales de IpCursorName son los siguientes:

IDC_APPSTARTING Standard arrow and small hourglass 
IDC_ARROW Standard arrow 
IDC_CROSS Crosshair 
IDC_HAND Windows NT 5.0 and later: Hand 
IDC_HELP Arrow and question mark 
IDC_IBEAM I-beam 
IDC_ICON Obsolete for applications marked version 4.0 or later. 
IDC_NO Slashed circle 
IDC_SIZE Obsolete for applications marked version 4.0 or later. Use IDC_SIZEALL. 
IDC_SIZEALL Four-pointed arrow pointing north, south, east, and west 
IDC_SIZENESW Double-pointed arrow pointing northeast and southwest 
IDC_SIZENS Double-pointed arrow pointing north and south 
IDC_SIZENWSE Double-pointed arrow pointing northwest and southeast 
IDC_SIZEWE Double-pointed arrow pointing west and east 
IDC_UPARROW Vertical arrow 
IDC_WAIT Hourglass

GetStockObject(): Es utilizado para extraer un objeto grafico del stock predefinido del GDI de Windows, permitiéndonos utilizar recursos incluidos dentro del propio Windows, como la brocha(brush), paleta (palette), fuentes(font), plumas(pen), etc.

HGDIOBJ GetStockObject(
  int fnObject   // entero con el valor del objeto
);

En nuestro caso la hemos utilizado para extraer la brocha blanca con la que rellenar el fondo de nuestra ventana (indicándola en wcx.hbrBackGround()).


LA FUNCION CREATEWINDOWEX()

La función CreateWindowEx(), como se comento en el tutorial anterior, crea la ventana tras registrarla. Su prototipo es el siguiente:

HWND CreateWindowEx(
  DWORD dwExStyle,      // estilo extendido de la ventana
  LPCTSTR lpClassName,  // puntero al nombre de la clase
  LPCTSTR lpWindowName, // puntero al titulo de la ventana
  DWORD dwStyle,        // estilo de la ventana
  int x,                // posicion orizontal de la ventana
  int y,                // posicion vertical de la ventana
  int nWidth,           // ancho de la ventana
  int nHeight,          // alto de la ventana
  HWND hWndParent,      // handle a la ventana padre
  HMENU hMenu,          // handle al menu (NULL=no menu)
  HINSTANCE hInstance,  // handle a la instancia
  LPVOID lpParam        // puntero a los datos de creacion
);

De esta declaración podemos deducir fácilmente el significado de (x, y) y (nWidth, nHeight) como la posición y el tamaño de la ventana en pixels (referido a la esquina (0, 0) del escritorio (o pantalla)). El parámetro lpClassname es el nombre de la clase con la que registramos la ventana y hInstance es un handle a la instancia actual de la aplicación (cuando ejecutamos varias veces un mismo programa, cada ventana es una nueva instancia del mismo). Mucho de esos parámetros se ponen a NULL, como por ejemplo al no disponer de ventana padre.
Por otra parte, dwExStyle y dwStyle nos va a permitir elegir el aspecto de nuestra ventana.


CUADROS DE DIALOGO

Una de las funciones de ventana mas útiles de Windows es la posibilidad de mostrar y gestionar cuadros de dialogo del estilo Si/No. Aceptar/Cancelar, etc, muy útiles para mostrar información, resultado de errores, para pedir confirmaciones al usuario, permitiendo un rápido y sencillo control del flujo del programa.
En el tutorial anterior pudimos ver en el Listado 1 un ejemplo de cuadro de dialogo de información, que esperaba la pulsación del botón Aceptar para continuar la ejecución. La creación de uno de estos cuadros de dialogo es muy sencilla gracias a MessageBox() y MessageBoxEx() (como siempre el prefijo Ex significa Extendido), funciones de la API de Windows para la gestión de los mismos. Los cuadros de dialogo pueden ser "modales" y "modales de sistema". La diferencia entre ambos radica en que los primeros no permiten acceder a otra ventana del programa hasta que la respuesta del usuario se produzca aunque permite el acceso a otros programas distintos del mismo (estos son los cuadros por defecto) Por otra parte, los segundos no permiten el acceso a ninguna otra aplicación (incluida la nuestra) hasta que son respondidos. Veamos el prototipo de la función MessageBox():

int MessageBox(
  HWND hWnd,          // handle ventana
  LPCTSTR lpText,     // frase informacion
  LPCTSTR lpCaption,  // titulo
  UINT uType          // estilo del cuadro
);

De los cuatro parámetros, el primero es el handle a la ventana padre (que puede ser NULL si no queremos que pertenezca a ninguna ventana en concreto), el segundo (lpText) un puntero al texto que queremos que aparezca en el (Windows partirá la línea si es necesario) pudiendo además incluir secuencias de escape como \n, etc. El tercero constituye el titulo de la ventana y el ultimo (y mas importante) el estilo del dialogo, es decir, los botones y el tipo de dialogo deseado. Podemos usar usar varios de estos indicadores simultáneamente empleando el operador OR Como resultado, MessageBox nos devuelve el botón que haya sido pulsado (ya sea como ratón o teclado) , o cero en caso de error.
Un ejemplo del uso de MessageBox se han implementado en el Listado 1.

//Listado 1

#include <windows.h>

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

 opcion = MessageBox( NULL,
		       "Texto de información.",
		       "Título del MessageBox.",
				MB_OKCANCEL | MB_ICONASTERISK );

 if( opcion == IDOK )
	MessageBox(NULL, "Ha pulsado ACEPTAR",
		                  "Salir", MB_OK );
 else
		MessageBox(NULL, "Ha pulsado CANCELAR",
		                 "Salir", MB_OK );
 return(0);
}

LAS MACROS LOWORD E HWORD

LOWORD e HWORD son dos funciones de tipo macro predefinido para Windows que nos permiten extraer el WORD bajo y alto, respectivamente de cualquier variable de 32 bits que se le especifique. Esto resulta muy útil muchas veces para obtener parámetros pasados a la función de procedimientos de ventana. Veamos como ejemplo una opción de código que nos permite saber sobre el tamaño de nuestra ventana cuando cambia su tamaño (mensaje WM_SIZE, recibimos en IPamam el nuevo ancho y alto):

int Anchi, Alto;
// ... codigo ...
case WM_SIZE
	Ancho=LOWORD(lParam);
	Alto=HWORD(lParam);

Cuando el mensaje recibido es WM_SIZE , IParam se obtiene el nuevo tamaño del área cliente de la ventana , estando en la parte alta de IParam la anchura y en el WORD bajo, la altura.


LA FUNCIÓN TEXTOUT(): TEXTO EN NUESTRA APLICACIÓN

Una de las funciones mas importantes para hacer nuestras pruebas y programas es la salida de texto en la pantalla, muy útil para debbugear la aplicación y obtener mensajes en la ventana de la misma . Para ello se dispone de la función TextOut ( aparte de la DrawText vista en el ejemplo anterior):

BOOL TextOut(
  HDC hdc,           // handle al DC
  int nXStart,       // coordenada x 
  int nYStart,       // coordenada y
  LPCTSTR lpString,  // puntero al texto
  int cbString       // longitud de la cadena
);

Los parámetros a pasarle a la función son un handle de contexto de dispositivo (que podemos obtener mediante BeginPaint() y bien, como veremos en el próximo tutorial mediante GedDC), las coordenadas (en pixels) donde imprimir la cadena , la propia cadena, y la longitud en caracteres pues esta función no busca el carácter END OF STRING (0) al final de la misma.
Por supuesto necesitamos saber la anchura en pixels de la fuente para poder escribir las líneas unas debajo de las otras ( o caracteres en posiciones concretas de la ventana, haciendo una "posición actual" a modo de cursor donde escribir, como en MS-DOS Para ello necesitamos saber la altura y la anchura de cada carácter para la fuente que este seleccionada para nuestra aplicación (la System Font, por defecto). El mejor momento de hacer esto es durante la creación de la aplicación , tomando la altura y la anchura de cada carácter mediante la función GetTextMetrics(), como puede verse en el listado 2 , donde se utilizan unas variables del tipo static (no desaparece su valor al salir de la función) llamadas cxChar y cyChar , que contiene el ancho y alto medio de cada carácter para la sytem font.

//-----------
// Listado 2
//-----------
#include <windows.h>

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

//--- Declaración de variables del programa -----------
char WindowName[]  = "Default Window";
char WindowTitle[] = "Windows95 application";

//=== 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:
  wcx.cbSize = sizeof( WNDCLASSEX );
  wcx.style = CS_HREDRAW | CS_VREDRAW;
  wcx.lpfnWndProc = WndProc;
  wcx.cbClsExtra = 0;
  wcx.cbWndExtra = 0;
  wcx.hInstance = hInstance;
	
  // icono, cursor, fondo e icono pequeño:
  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.lpszClassName = WindowName;
  wcx.lpszMenuName = NULL;

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

  // Creamos la ventana con CreateWindowEx():
  hwnd = CreateWindowEx(
    WS_EX_OVERLAPPEDWINDOW,
    WindowName, WindowTitle,
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT,
    400, 300, NULL, NULL,
    hInstance, NULL
                     );

  // Comprobamos la creación de la ventana:
  if( !hwnd )
    return( FALSE );

  // 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 );
      DispatchMessage( &msg );
  }

  // 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;
  TEXTMETRIC tm;
  RECT rect;
  int loop;
  static long cxChar, cyChar;

  switch( message )
  {
    // mensaje producido en la creación de la ventana
    case WM_CREATE: 
        hdc = GetDC( hwnd );
        GetTextMetrics( hdc, &tm );
        cxChar = tm.tmAveCharWidth;
        cyChar = tm.tmHeight + tm.tmExternalLeading;
        ReleaseDC( hwnd, hdc );
        break;

    case WM_PAINT:
        hdc = BeginPaint( hwnd, &ps );
        GetClientRect( hwnd, &rect );
        for( loop=0; loop<10; loop++ )
          TextOut( hdc, 0,
                   loop*cyChar,
                   "ESTO ES UNA PRUEBA", 18 );
        EndPaint( hwnd, &ps );
        break;

    // mensaje producido al aumentar el tamaño de la ventana
    case WM_SIZE:
          // aqui habriamos de coger de nuevo el tamaño.
        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);
}

TEXTOUT Y WSPRINTF

La forma mas sencilla de utilizar TextOut() es conjuntamente con la función wsprintf(). Esta función introduce cadenas y variables (admitiendo especificadores de formato) dentro de un buffer de texto (los concatena), devolviendo la longitud , a titulo de ejemplo:

char cadena[80];
int longitud;
//... codigo ...
longitud=wsprintf(cadena, "El resultado es %d", total);
TextOut(hdc, 100, 100, cadena, longitud);

De esta manera , en nuestra aplicación podemos imprimir tanto cadenas de texto puro como valores numéricos de cualquier tipo, aprovechando además que wsprintf() nos devuelve el tamaño de la cadena creada.


EL BUCLE DE MENSAJES

El bucle de mensajes es sin duda uno de los puntos vitales de nuestro programa, ya que en el se recogen y traducen todos los mensajes de Windows a los parámetros que finalmente reciben la función callback de procedimientos de ventana:

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

Este es un bucle que se repetirá hasta que el usuario salga de la aplicación momento en el que recibiremos un cero y el while será FALSE En casi de recibir un valor distinto de 0 (TRUE) , el bucle continua Hay que decir que Windows tiene una cola de mensajes , existiendo una continua llegada y traducción de los mismos.

BOOL GetMessage(
  LPMSG lpMsg,         // puntero a la estructura que contiene el mensaje recibido
  HWND hWnd,           // handle a la ventana que lo recibe
  UINT wMsgFilterMin,  // identificador del primer mensaje
  UINT wMsgFilterMax   // identificador del ultimo mensaje
);

El tipo de datos LPMSG es (vayámonos acostumbrándonos a la terminología "húngara" de Windows) un puntero largo a MSG (LP=Long Pointer):

typedef struct tagMSG {     // msg 
    HWND   hwnd;     // handle a la ventana destinataria 
    UINT   message;    //identificador del mensaje
    WPARAM wParam;    // informacion adicional
    LPARAM lParam;    // informacion adicional
    DWORD  time;    // hora del envio del mensaje
    POINT  pt;    // coordenadas del raton
} MSG;

WPARAM es el mismo tipo que UINT y LPARAM es un typedef para LONG , la estructura tipo POINT contiene las coordenadas del ratón en el momento de recibir el mensaje y esta definida como:

typedef struct tagPOINT
{
	LONG x;   // coordenada x
	LONG y:   // coordenada y
}POINT;

Como puede verse en la línea GetMessage(&msg, NULL, 0, 0) handle a la ventana puede especificarse como NULL, ylparam/wparam a cero con el fin de obtener todos los mensajes para nuestra aplicación, no solo par una determinada ventana ( si hubiesemos creado diferentes ventanas hijas). Continuando con el comentario del bucle del mensajes, mediante TransalateMessage() traducimos algunos de estos mensajes, y mediante DispatchMessage() los enviamos a la función de procedimientos de ventana para que realice su proceso.


OTRA MANERA DE RECOGER LOS MENSAJES

Bajo Windows tenemos otra posibilidad en ves de esperar a que Windows nos envie un mensaje, continuar trabajando (aunque no nos envié mensajes) recogiendo nosotros mismos todos los mensajes que se envíen en Windows y contestando tan solo a los que nos atañen. Para ello puede usarse la función PeekMessage(), lo cual nos permitirá realizar otras acciones (como actualizar nuestro programa) una vez por cada mensaje que sea enviado a Windows.

while(1)
{
 HacerAlgo();
 if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
 	{
	if (msg.message==WN_QUIT)
		return (msg.wParam);
	TranslateMessage(&msg);
	DispatchMessage(&msg);
	}
}

Mediante el bucle anterior hacemos que nuestra función vaya en busca de los mensajes al mismo tiempo que ejecutamos código propio de nuestra aplicación (llamando en el ejemplo anterior de manera ilustrativa HacerAlgo()), que podría, en el caso de un juego redibujar la pantalla, mover los sprites a distintas posiciones , con esto le estamos robando tiempo a otras aplicaciones.


Códigos fuentes de los dos ejemplos de este tutorial

Los ejemplos los compile con el VisualC++ 6.0

Listado 1
Listado 2

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

1