[ Contenidos | #winprog ]

Una Simple Ventana.

Ejemplo: simple window

[images/simple_window.gif] A veces las personas ingresan al IRC y preguntan "¿Como hago una ventana...?". Bien, no es tan simple como parece. No es difícil una vez que sepas lo que estás haciendo, pero hay algunas cosas que necesitas hacer para poder crear un ventana. Y son mas de las que pueden ser explicadas en una sala de chat o en una nota rápida.

Siempre me gustó hacer las cosas primero y aprenderlas luego... por lo tanto aqui está el código de una simple ventana que será explicado en breve.

#include <windows.h>

const char g_szClassName[] = "myWindowClass";

// Step 4: the Window Procedure
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc;
    HWND hwnd;
    MSG Msg;

    //Step 1: Registering the Window Class
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, "Window Registration Failed!", "Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    // Step 2: Creating the Window
    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        g_szClassName,
        "The title of my window",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 240, 120,
        NULL, NULL, hInstance, NULL);

    if(hwnd == NULL)
    {
        MessageBox(NULL, "Window Creation Failed!", "Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // Step 3: The Message Loop
    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

Generalmente este es el programa más simple que puedes escribir par crear una ventana funcional, digamos unas 70 líneas de código. Si has compilado el primer ejemplo, entonces este debería funcionar sin problemas.

Paso 1: Registrar la Clase Ventana

Una Clase Ventana almacena información sobre el tipo de ventana, incluyendo su Window Procedure, los íconos grandes y pequeños y el color de fondo. De esta forma podemos registrar una clase una vez y luego crear muchas ventanas a partir de ésta, sin tener que especificar todos los atributos una y otra vez. Muchos de los atributos que especificamos en la Clase Ventana pueden ser cambiados, si se desea, en una base per-windows.

La Clase Ventana no tiene nada que ver con las clases de C++.

const char g_szClassName[] = "myWindowClass";
La variable anterior almacena el nombre de nuestra Clase Ventana y la usaremos en breve para registrar nuestra clase en el sistema.
  WNDCLASSEX wc;  
  wc.cbSize = sizeof(WNDCLASSEX); 
  wc.style = 0;
  wc.lpfnWndProc = WndProc; 
  wc.cbClsExtra = 0; 
  wc.cbWndExtra = 0;
  wc.hInstance = hInstance; 
  wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor = LoadCursor(NULL, IDC_ARROW); 
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); 
  wc.lpszMenuName = NULL; 
  wc.lpszClassName =g_szClassName; 
  wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

  if(!RegisterClassEx(&wc)) 
  { 
    MessageBox(NULL, "Window Registration Failed!", "Error!", MB_ICONEXCLAMATION | MB_OK); 
    return 0; 
  }
  

Este es el código que usamos en WinMain( ) para registrar nuestra Clase Ventana en el sistema. Para ello, rellenamos los campos de una estructura WNDCLASSEX y llamamos a RegisterClassEx( ). Los campos de la estructura son los siguientes:

cbSize
El tamaño de la estructura
style
El estilo de la clase (CS *), no confundirse con el estilo de la ventana (WS *). Generalmente este campo puede ser puesto en cero.
lpfnWndProc
Puntero al Window Procedure de esta clase.
cbClsExtra
Cantidad extra de asignación de memoria para datos de la clase. Generalmente cero.
cbWndExtra
Cantidad extra de asignación de memoria por ventana de este tipo. Generalmente cero.
hInstance
Handle a la instancia de la aplicación (la que obtenemos en el primer parámetro de WinMain( ) )
hIcon
Handle al ícono grande (32x32), mostrado cuando el usuario presiona Alt+Tab.
hCursor
Handle al cursor que será mostrado sobre la ventana.
hbrBackground
Pincel para fijar el color de fondo de nuestra ventana.
lpszMenuName
Nombre del recurso Menú para usar con las ventanas de esta clase.
lpszClassName
Nombre para identificar la clase.
hIconSm
Handle al ícono pequeño (16x16), usado en la barra de tareas y en la esquina superior izquierda de la ventana.
No te preocupes si esto no queda muy claro aún, las distintas partes que vimos serán explicadas mas adelante. Otra cosa para recordar es no intentar memorizar toda esta estructura. Yo raramente (de hecho nunca) memorizo estructuras o parámetros de funciones, esto es un desperdicio de tiempo y esfuerzo. Si conoces las funciones que necesitas llamar, entoces es una cuestión de segundos mirar los parámetros exactos en la ayuda de tu compilador. Si no tienes los archivos de ayuda entonces consigue algunos porque sin ellos estas perdido. Eventualmente irás aprendiendo los parámetros de las funciones que mas usamos.

Luego llamamos a RegisterClassEx( ) y chequeamos por alguna falla, si hubo alguna mostramos un mensaje y abortamos el programa retornando a la función WinMain( ).

Paso 2: Crear la Ventana:

Una vez que la clase ha sido registrada, podemos usarla para crear una ventana. Observemos los parámetros de la función CreateWindowEx( ) (como lo haremos con cada nueva llamada a la API que veamos), los explicaré brevemente:

    HWND hwnd;
    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        g_szClassName,
        "The title of my window",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 240, 120,
        NULL, NULL, hInstance, NULL);

El primer parámetro (WS_EX_CLIENTEDGE) es el estilo extendido de la ventana, en este caso he configurado éste para darle un borde interno y hundido al marco de la ventana. Pon este valor en cero para observar la diferencia, o prueba con otros valores para ver que hacen.

A continuacióm tenemos el nombre de la clase (g_szClassName), éste le dice al sistema que tipo de ventana crear. Debido a que queremos crear una ventana a partir de la clase que hemos registrado, usamos el nombre de dicha clase. Luego especificamos el nombre de nuestra ventana o título que aparecerá en la barra de título de la ventana.

El parámetro que tenemos como WS_OVERLAPPEDWINDOW es estilo de la ventana. Hay algunos de éstos y deberías mirarlos y experimentar para observar que hacen. De todos modos serán cubietos mas adelante.

Los siguientes cuatro parámetros ( CW_USEDEFAULT, CW_USEDEFAULT,320,240) son las coordenadas X e Y de la esquina superior izquierda de la ventana y el alto y ancho de de la ventana, respectivamente. He puesto los campos X e Y con el valor CW_USEDEFALT para dejar que windows elija en qué lugar de la pantalla ubicar la ventana. Recuerda que la parte izquierda de la pantalla tiene un valor de X igual a cero y se incrementa hacia la derecha y la parte superior de la pantalla tiene un valor cero de Y y se incrementa cuando avanzamos hacia abajo en la pantalla. Las unidades son los pixeles, el cual es la miníma unidad que una pantalla puede mostrar en una resolución determinada.

Los siguientes parámetros (NULL,NULL,g_hInstance,NULL) corresponden al handle de la ventana padre, al handle del menú, al handle de la instancia de la aplicación y un puntero a los datoscon los cuales se creó la ventana. En windows las ventanas son ordenadas en la pantalla de acuerdo a una jerarquía de padre e hijo. Cuando ves un botón en una ventana, el botón es el Hijo de la ventana en la cual está contenido y dicha ventana es su Padre. En este ejemplo, el handle al padre es nulo debido a que no tenemos padre, estamos en el nivel principal de las ventanas. El handle al menú es nulo debido a que, por ahora, no tenemos menú. El handle a la instancia de la aplicación es puesto con el valor que es pasado en el primer parámetro del WinMain( ). Los datos de creación (los culaes nunca uso) pueden ser usados para enviar información adicional a la ventana que está siendo creada. También es nulo. Si estás preguntándote que es la palabra mágica NULL, es simplemente definida como cero (0). En realidad, en C está definida como ((void *) 0), debido a que está destinada a ser usada con punteros. Por lo tanto probablemente obtengas algunos "warnings" si la utilizas para valores enteros, de todas maneras esto depende del compilador y del nivel de warnings en la configuaración del mismo. Puedes elegir ignorar los warnings o usar el valor cero (0)en lugar de NULL.

La principal causa de que las personas no conozcan cuales son las fallas de sus programas es que no chequean los valores de retorno de las llamadas a funciones para determinar si éstas han fallado o no. CreateWindow( ) puede fallar aún si eres un experimentado programador debido a que hay una gran cantidad de errores que son fáciles de cometer. Hasta que aprendas como identificar rápidamente dichos errores date la oportunidad de resolverlos y siempre chequea los valores de retorno!

    if(hwnd == NULL)
    {
        MessageBox(NULL, "Window Creation Failed!", "Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

Después que hemos creado la ventana y hemos chequeado para asegurarnos de que tengamos un handle válido, mostramos la ventana utilizando el último parámetro provisto en WinMain( ) y luego lo actualizamos para asegurarnos que se halla redibujado en la pantalla a si mismo de manera apropiada.

   ShowWindow(hwnd, nCmdShow);
   UpdateWindow(hwnd);

El parámetro nCmdShow es opcional, simplemente puedes pasar el valor SW_SHOWNORMAL en todo momento y listo. Sin embargo si usamos el parámetro provisto en WinMain( ) le da a quien esté corriendo tu programa la oportunidad de especificar si quiere que la ventana comienze visible, maximizada, minimizada, etc... Encontrarás opciones para este parámetro en las propiedades de los iconos del escritorio y apartir de estos valores será determinado el valor que tome el parámetro nCmdShow.

Paso 3: El Bucle de Mensajes

Este es el corazón del programa, la mayoria de las cosas que tu programa realiza pasan a través de este punto de control.

    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;

GetMessage() obtiene un mensaje de la cola de mensajes de tu aplicación. Cada vez que el usuario mueve el mouse, presiona el teclado, hace click en el menú de la ventana , etc... el sistema genera mensajes que ingresan en la cola de mensaje de la tu aplicación. Utilizando GetMessage( ) estás solicitando que el siguiente mensaje disponible en la cola sea removido de la misma y te sea entregado para procesarlo. Si no hay mensajes, GetMessage( ) se bloquea . Si no estás familiarizado con dicho término, esto significa que espera a que halla un mensaje en la cola y luego te lo retorna.

TranslateMessage( ) hace algunos procesos adicionales en los eventos del teclado, como generar mensajes WM_CHAR que van junto a los mensajes WM_KEYDOWN. Finalmente DispatchMessage( ) envía el mensaje a la ventana que había recibido dicho mensaje. Esta podría ser nuestra ventana principal, otra ventana, o un control e incluso una ventana que fué creada "detrás de la escena" por el sistema o algún otro progama. Esto no es algo que debería precocuparte debido a que lo único que nos interesa saber es que obtenemos el mensaje y lo envíamos, el sistema se encarga de hacer el resto asegurándose de trabajar con la ventana correcta.

Paso 4: El Windows Procedure

Si el bucle de mensajes es el corazón del programa entonces el Windows Procedure es el cerebro. Aqui es donde todos los mensajes son envíados para que sean procesados por nuestra ventana.
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

El Windows Procedure (pronunciado "winprock") es llamado por cada mensaje que procesamos, el parámetro HWND es el handle dela ventana a la cual se aplica el mensaje. Esto es importante debido a que puedes tener dos o mas ventanas de la misma clase que usarán el mismo Windows Procedure (WndProc( ) ), la diferencia es que el parámetro hWnd será diferente dependiedno de la ventana. Por ejemplo cuando obtenemos el mensaje WM_CLOSE (cerrar ventana) destruimos la ventana y debido a que usamos el handle de la ventana (que recibimos en el primer parámetro) sabremos que ventana es la que hay que destruir. Las demás ventanas no se verán afectadas, solo la que recibió el mensaje.

WM_CLOSE es envíado cuando el usuario presiona ALT+F4 o cuando presiona el botón [x]. Esto causará que la ventana sea destruida por default, pero prefiero manejarlo explicitamente debido a que es el punto perfecto para realizar "limpiezas" o preguntarle al usuario si desea guardar los cambios, etc...antes que la ventana se destruida. (i.e el programa finalice).

Cuando llamamos a DestroyWindow( ) el sistema envía el mensaje WM_DESTROY a la ventana que va a ser destruída, que en este caso es nuestra ventana, y por lo tanto destruimos todas las ventanas hijas antes de remover nuestra ventana del sistema. Debido a que esta es nuestra única ventana en nuestro programa todo lo que hacemos es salir del mismo, para ello utilizamos la llamada PostQuitMessage( ). Esta envía el mensaje WM_QUIT al bucle de mensajes pero nunca recibimos este mensaje debido a que éste causa que GetMessage( ) retorne FALSE y como vimos en el código del Bucle de Mensajes, cuando esto sucede, dejamos de procesar mensajes y retornamos el código resultante final, el Wparam del WM_QUIT el cual es el valor pasado en PostQuitMessage( ). El valor de retorno solo es usado si tu programa está diseñado para ser llamado por otro programa y quieres que retorne un valor especifico.

Paso 5: No hay paso 5.

Bien, eso eso fué todo!(por ahora). Si las cosas aún no se han entendido del todo, solo sigue adelante y espera que las cosas se tornen mas claras, como cuando veamos algunos programas.


Copyright © 1998-2003, Brook Miles (theForger). All rights reserved.

Versión en Español Federico Pizarro - 2003