Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


 Prev.

1.7.5  Programación Windows (II)

§3  Esquema de un programa básico

Aunque la versión del "Hola mundo" presentada en la página anterior, es desde luego un programa que goza de todas las características "Windows", -produce una salida gráfica, cuya ventana puede ser movida (arrastrada) por el escritorio, y ser manejada (cerrada) con el ratón-. Sin embargo, no es precisamente una versión representativa de la estructura de tales programas. La razón es que está construido con una ventana muy especial, obtenida con la función MessageBox de la API, que ofrece una funcionalidad mínima.

El esquema de un programa básico, pero que contuviese las características principales de la programación Windows, podría ser el siguiente:

#include <windows.h>

LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM); // Declara una función (definida al final)

char szClassName[ ] = "WindowsApp";    // declara una variable global

int WINAPI    WinMain (         // Inicio de la aplicación [0]
    HINSTANCE hThisInstance,
    HINSTANCE hPrevInstance,    // NULL en los programas para Window 32 bits
    LPSTR     lpszArgument,     // Argumento pasado al programa (cadena de caracteres)
    int       nFunsterStil ) {  // entero que puede ser pasado a la funcion ShowWindow()

    HWND hwnd;                  // Manejador ("handle") de la nueva ventana
    WNDCLASSEX wincl;           // Declarar una estructura (la clase-Windows)

//  Paso 1: Definir la clase-Windows (la estructura anterior)
    wincl.hInstance = hThisInstance;   // Manejador ("handle") a la instancia en ejecución [1]
    wincl.lpszClassName = szClassName; // Nombre que identifica la clase
    wincl.lpfnWndProc = WindowProcedure;   // Puntero a una función [2]
    wincl.style = CS_DBLCLKS;              // Indica que la aplicación capturará doble-clicks
    wincl.cbSize = sizeof (WNDCLASSEX);    // Tamaño de la estructura

    wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);   // Icono que usará la aplicación [3]
    wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION); // Ídem [4]
    wincl.hCursor = LoadCursor (NULL, IDC_ARROW);     // Cursor que utilizará la ventana [5]
    wincl.lpszMenuName = NULL;                        // Menú que utilizará la ventana [6]

    wincl.cbClsExtra = 0;                  // Datos adicionales [7]
    wincl.cbWndExtra = 0;                  // Datos adicionales [8]
    wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;  // Color de fondo [9

//  Paso 2: Registrar la clase-Windows
    if (!RegisterClassEx (&wincl)) {       // Registrar la clase [10]
        // Mensaje de error si falla el "registro"
        MessageBox( NULL, "Error al registrar la clase!", "Error!",
                    MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

//  Paso 3: Crear la ventana [11]
    hwnd = CreateWindowEx (
        0,
        szClassName,
        "Mi primera aplicación",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 280, 160,
        HWND_DESKTOP,
        NULL,
        hThisInstance,
        NULL);

//  Paso 4: verificar que el registro se ha realizado correctamente
    if (hwnd == NULL) {
      MessageBox (NULL, TEXT ("Ha fallado la creación de la ventana!"),
                  TEXT("Error!"), MB_ICONEXCLAMATION | MB_OK);
      return 0;   // Terminar la aplicación
    }

    ShowWindow (hwnd, nFunsterStil);    // Mostrar la ventana en el escritorio


// Paso 5: Bucle de mensajes [12]
    MSG messages;     // información sobre los mensajes recibidos por la aplicación
    while (GetMessage (&messages, NULL, 0, 0)) {
        TranslateMessage(&messages);
        DispatchMessage(&messages);
    }

//  Paso 6: terminar el programa [13]
    return messages.wParam;
}


//  definición de procedimiento Windows [14]
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

    switch (message) {    // maneja los mensajes recibidos
        case WM_CLOSE:
            DestroyWindow(hwnd);
            break;
        case WM_DESTROY:
            PostQuitMessage (0); /* post a WM_QUIT message to the message queue */
            break;
        default:
            break;
    }
    return DefWindowProc (hwnd, message, wParam, lParam);
}

 

Como resultado de su ejecución, aparece en el escritorio una ventana como la figura adjunta, que dispone de las características de una aplicación Windows estándar: botones de maximizar; minimizar y restaurar; una barra de título, y el correspondiente menú debajo del icono superior izquierdo.  Pulsando sobre el botón de cerrar, el programa termina y la ventana es destruida, desapareciendo del escritorio.

Una explicación más detallada (en inglés) puede obtenerse en el sitio de Microsoft: Creating Win32 Applications (C++)

  Inicio.


[0]  Incluso en las aplicaciones C/C++ para Windows, la función inicial no es main, sino WinMain, que como vemos, tampoco tiene la misma "firma" (número y tipo de parámetros) que aquella. El asunto lo hemos tratado en el capítulo correspondiente ( 4.4.4).

[1]  Observe que este miembro de la clase-Windows (Windows class), que identifica a la instancia de nuestro programa, es precisamente el primero de los argumentos recibidos por WinMain. Posteriormente esta información será reenviada al Sistema en el proceso de "registrar la clase" [10] (la función RegisterClassEx pasa un puntero a nuestra Windows class), y la función CreateWindowEx lo vuelve a utilizar como argumento [11].  Se trata por tanto de una situación aparentemente absurda, ya que enviamos al Sistema información que este nos ha enviado previamente.  La razón del aparente sinsentido reside en que, al ser Windows un sistema multiprogramación, en un momento dado pueden estar ejecutándose varias instancias de una misma aplicación (cada instancia dispone de su propia "ventana" que es el paradigma de cada proceso).  Sin embargo, las aplicaciones Windows son "reentrantes"; lo significa que, por razones de eficacia, el Sistema no carga en memoria una nueva imagen completa para cada instancia, solo del segmento de datos, mientras que el segmento de código es compartido entre todas las instancias.  El resultado es que, si se están ejecutando dos instancias de una misma aplicación, una misma copia de WinMain está sirviendo a las dos, y Windows necesita saber a cual de ellas sirve en cada momento para pasarle los mensajes correspondientes.  Por esta razón, cada vez que Windows inicia un nuevo proceso le asigna un número de instancia. Este valor, que es recibido mediante un argumento de WinMain; se mantiene constante a lo largo de toda la vida del proceso, y debemos utilizarlo en muchas ocasiones en que queramos comunicar con el Sistema.  En consecuencia, suele ser almacenado en forma de una variable estática global, visible desde todos los módulos de la aplicación (observe que no puede ser una constante estática porque su valor es determinado en runtime).

Nota:  en cierta forma, este primer argumento de WinMain recuerda al puntero this de los métodos C++, que permiten identificar a cada instancia de la clase ( 4.11.6).

[2]  Esta función es denominada Procedimiento Windows (Window procedure), aunque en la programación C/C++ no es ningún "Procedure", sino una función.  Cada clase (window class) dispone de una de estas funciones; su peculiaridad es que es invocada por el sistema operativo, constituyendo la vía de comunicación entre el Sistema y nuestra aplicación Windows (en ella recibimos la información que nos envía el Sistema).  Por esta razón es de un tipo especial; devuelve un BOOL (que en la terminología Windows en un LRESULT); recibe cuatro argumentos de tipos concretos, y está definida como CALLBACK, lo que significa que sigue la convención de llamada denominada "Estándar" ( 4.4.6a).  Más información en la nota [14].

[3]   Indicación del tipo de icono que utilizará la aplicación.  Windows proporciona algunos que pueden ser utilizados por defecto, como es nuestro caso. Existen varios tamaños que son usados según las circunstancias del momento.  Este se refiere al icono grande, generalmente de 32x32 pixels, que es mostrado cuando el usuario pulsa Alt+Tab.

[4]  Esta sentencia indica el icono pequeño que utilizará la aplicación. Generalmente de 16x16 pixels, Es el icono que aparece en la barra de tareas cuando la aplicación está en ejecución; también el que aparece en la esquina superior izquierda de la ventana (extremo izquierdo de la barra superior de título -Title-bar-).

[5]  Cada ventana puede utilizar su propio diseño para el cursor que señala la posición del ratón. Windows proporciona un surtido estándar, pero la aplicación puede utilizar diseños propios.  Generalmente es la consabida flecha, pero puede ser otro. Por ejemplo, la cruz que aparece en algunos programas de diseño gráfico.  También puede cambiar en runtime. Por ejemplo, pasar a ser un reloj de arena para indicar que la aplicación está ejecutando un proceso y debemos esperar.

[6]  Se refiere al menú de opciones que aparece en la ventana, inmediatamente debajo de la barra de título (caption bar), la barra superior donde aparece el título de la aplicación, que permite mover la ventana. En este caso la aplicación no utiliza ningún menú (NULL).

[7]  Señala el tamaño de almacenamiento extra que se reserva en memoria para esta clase. Generalmente suele ser cero.

[8]  Señala el tamaño de almacenamiento extra que se reserva para cada ventana de esta clase (recordar que en una misma aplicación varias ventanas pueden compartir una misma clase).  Generalmente este valor es cero.

[9]  Indica el color del pincel (Brush) que se utilizará para el fondo de la ventana.  Windows dispone de varios colores estándar, aunque la aplicación puede definir un color propio.

[10]  El proceso de "registrar" la clase, es en realidad una forma de pasar al Sistema información sobre las características de nuestra aplicación. Para ello se utiliza una función especial RegisterClassEx, con la que le pasamos un puntero a nuestra window class (que contiene los datos pertinentes).  De esta forma el SO puede conocer la dirección de nuestro windows procedure para invocarlo cada vez que necesita enviarnos un mensaje. También otros datos, como el color de fondo que utilizará para dibujar la ventana; el diseño de puntero para el cursor del ratón; los iconos, etc.

Nota:  el miembro lpszClassName (nombre) de la window class debe ser único para cada clase que registremos en nuestra aplicación.

[11]  Aunque en la terminología Windows se utiliza la expresión "crear la ventana", en realidad se refiere a solicitar al sistema que inicie el proceso que gobernará nuestra aplicación, y cuya expresión gráfica en el escritorio es una ventana (la ventana es en realidad una metáfora o alegoría de la aplicación). Para esto se utilizan las funciones CreateWindow o CreateWindowEx, que utilizan un número de parámetros inusualmente elevado. Entre otros datos, se indican en ellos el nombre de la aplicación; estilo de la ventana; tamaño inicial y coordenadas (respecto del escritorio) donde aparecerá, etc. Por ejemplo, en nuestro caso, la ventana tendrá 280 pixels de ancho y 160 de alto, pero hemos dejado que sea el Sistema el que decida las posiciones (X,Y) iniciales de su vértice superior izquierdo (CW_USEDEFAULT).  Observe que uno de los argumentos es el nombre de la Windows class que se registró previamente; la clase-Windows es utilizada por aquí por CreateWindowEx como una especie de plantilla, o molde, que define algunos aspectos de la nueva ventana.

Observe que el penúltimo argumento de CreateWindowEx es hThisInstance. Es decir, el identificador de la instancia a la que pertenece la ventana (nos hemos referido a él anteriormente [1]). De esta forma, Windows puede asociar cada ventana con la instancia correspondiente. 

[12]  Este es el denominado bucle de mensajes (message loop); uno de los elementos más característicos de un programa Windows, y lo que constituye realmente el núcleo de cualquier aplicación para este sistema operativo. Observe que el bucle se ejecuta mientras que la función GetMessage devuelva un valor TRUE (distinto de cero 3.2.1b).

El trasiego de información entre el sistema y la aplicación se realiza en forma de mensajes (paquetes de información de tamaño y estructura constante). Los mensajes pueden contener la información en sí mismos, o solo la dirección al sitio donde se encuentra la información (generalmente en forma de puntero a una estructura cuya disposición interna es conocida).  Para entender el esquema de funcionamiento hay que pensar que los mensajes pueden ser recibidos por la aplicación de dos formas:

  • El Sistema pone el mensaje en una cola (Thread queue) que es leída repetidamente por la aplicación.  Estos mensajes se conocen como encolados (Queued messages).

  • El Sistema invoca directamente al Windows procedure [14] de la aplicación (cuya dirección conoce) y pasa el mensaje en los argumentos de llamada. Esta forma se conoce como no encolada (Non-queued messages).

Por su parte, la aplicación también puede enviar mensajes a su propia cola o a la cola de otra aplicación, e incluso directamente al Windows procedure de otra aplicación. Es decir, la aplicación también puede enviarse a sí misma o a otra aplicación mensajes encolados y no encolados. Cuando se trata de mensajes encolados, la literatura de Microsoft se refiere a ellos como "post messages", y a los no encolados como "send messages".  Cuando la aplicación necesita enviar mensajes encolados a su propia cola o a la de otro proceso, se utilizan las funciones PostMessage y PostThreadMessage. Para los segundos se utilizan las funciones SendMessageSendMessageCallback (se diferencian es que SendMessage envía un mensaje al Windows procedure de la ventana correspondiente y espera hasta que recibe una respuesta, mientras que SendMessageCallback envía el mensaje y no espera que se complete la orden).  Por ejemplo, la sentencia PostQuitMessage (0); del programa anterior es una de estas funciones que coloca un mensaje en la cola; en concreto un mensaje WM_QUIT que provoca la terminación del bucle de mensajes.

Nota: como veremos más adelante, una aplicación no debe invocar directamente su propio Windows procedure, de forma que esta función solo debe ser invocada por el Sistema. 


El funcionamiento de los mensajes encolados es como sigue: la variable messages que hemos declarado antes, es de tipo MSG, que en Windows es en realidad el typedef de una estructura con 6 miembros (el último de los cuales es a su vez una estructura de dos miembros). Los miembros de MSG estan preparados para albergar la información del mensaje recibido, incluyendo la posición que tenía el cursor cuando se recibió el mensaje, momento en que se produjo, etc.  En cada iteración del message loop, la función GetMessage pasa al Sistema un puntero con la dirección de nuestra variable messages, de forma que de Windows puede colocar en ella la información pertinente al mensaje que nos envía. Estos mensajes proceden de una cola FIFO ( 1.8a) especial que mantiene el Sistema para nuestra instancia, conocida como cola de instancia (thread queue).  Existe un thread queue para cada instancia de cada aplicación en ejecución.  A su vez, los mensajes de las colas de instancia provienen de una cola general, conocida como cola del sistema (system queue) mantenida igualmente por el Sistema.  En esta última se reciben los mensajes procedentes de los controladores de dispositivos (device drivers). Por ejemplo, del teclado, del ratón, lápiz óptico, tarjetas de comunicaciones, Etc. Posteriormente, los mensajes son sacados del system queue para ser colocados en cada thread queue.

Nota: en Windows, todos los eventos que se producen en el Sistema son traducidos a mensajes, y estos mensajes son trasladados a las thread queue de las distintas aplicaciones en ejecución, aunque no de la misma forma, ya que Windows decide cual de las thread queue existentes en ese momento debe recibir cada mensaje del system queue.  Por ejemplo, los eventos producidos por el teclado y el ratón son enviados únicamente a la cola de la ventana que tiene foco (input focus, una propiedad que solo tiene una ventana cada vez) mientras que otros, como los producidos por el cronómetro del Sistema, sin enviados a todas las instancias.

Es importante destacar aquí que GetMessage es una función especial "de Windows" que comunica nuestra aplicación con el Sistema (no está definida en nuestra aplicación, sino en una DLL del Sistema) y que además de leer el mensaje, lo elimina de la cola. Además de esta, se dispone de una función, PeekMessage, que permite leer un mensaje sin borrarlo de la cola.

Mientras que el valor devuelto por GetMessage sea distinto de cero, nuestro bucle seguirá ejecutándose, lo que a la postre, significa que nuestra aplicación está en ejecución (observe que inmediatamente después de la salida del bucle de mensajes está el final del programa).

El segundo argumento de GetMessage señala la ventana de la aplicación que recibirá el mensaje (recuerde que una aplicación puede abrir una ventana principal -madre- y cualquier número de ventanas dependientes -hijas-).  Cuando este argumento es distinto de cero (diferente de NULL), solo la ventana correspondiente recibirá los mensajes. Como en nuestro ejemplo, este valor suele ser nulo, de forma que el bucle de mensajes recibe todos los destinados a cualquier ventana de la aplicación.

Nota: Windows clasifica los mensajes por tipos que se identifican mediante números. Los argumentos tercero y cuarto de GetMessage permiten realizar un filtrado de los mensajes que se recibirán, estableciendo los valores máximo y mínimo para los tipos. Generalmente estos valores son cero, de forma que GetMessage recibe cualquier tipo de mensaje que corresponda a la instancia. 

Dentro del bucle de mensajes existen dos invocaciones de función igualmente importantes. La primera, TranslateMessage, realiza cierta transformación en algunos mensajes, en especial los provenientes del teclado. Esta función pasa el mensaje al Sistema para que, en su caso, realice las transformaciones correspondientes. Si el mensaje no necesita transformación, el Sistema sencillamente ignora la petición.

Nota: Los mensajes generados cuando es pulsada o soltada una tecla, son conocidos como virtual-key messages. Se identifican con las constantes WM_KEYDOWN y WM_KEYUP. Estos mensajes contienen información muy completa sobre la tecla pulsada y sus circunstancias. Por ejemplo, si es la tecla ALT derecha o izquierda; si se han pulsado simultáneamente otra tecla, etc.  La aplicación puede atender estos mensajes y operar en consecuencia. Sin embargo, esto ocurre en contadas ocasiones, ya que la información recibida no suele ser directamente útil porque no contiene información sobre el carácter asociado a la tecla. Generalmente la aplicación necesita una "traducción" del mensaje de forma que TranslateMessage solicita al Sistema una comprobación de si la tecla corresponde a un valor ANSI, y en su caso, que lo transforme en un mensaje de tipo WM_CHAR (conocido como character message) que contiene el código ANSI de la tecla correspondiente. Por ejemplo, una letra; una cifra, etc.  Este mensaje es colocado en la cola de instancia y puede ser leído en una iteración posterior del bucle de mensajes.

La segunda de las funciones del bucle es DispatchMessage. Esta función solicita al sistema que reenvíe el mensaje al Windows procedure de la aplicación (ver a continuación). Es decir, la función solicita al Sistema que le envíe un mensaje no encolado (Non-queued).  Una característica de esta función es que no retorna hasta que el Windows procedure ha procesado el mensaje correspondiente.  En consecuencia, si el Windows procedure debe realizar una operación cuya ejecución es lenta, la iteración del bucle de mensajes queda suspendida hasta la conclusión de la misma.  Si en estas circunstancias deseamos inspeccionar la cola de mensajes. Por ejemplo, para dar al usuario la posibilidad de abortar la ejecución, debe habilitarse un mecanismo alternativo para consultar la cola de instancia, lo que puede efectuarse mediante distintas funciones previstas al efecto en el Sistema, como PeekMessageGetQueueStatus y GetInputState.

Nota:  el mecanismo de mensajes de Windows es bastante prolijo en detalles.  La mejor fuente de información que conozco es la documentación de Microsoft: "Message and Message Queue Overviews" donde puede encontrar una magnífica introducción al tema "About Messages and Message Queues" (preste especial atención a los detalles).  Una exposición más rica en detalles puede encontrase en el capítulo 15 de "The Old New Thing" (Addison-Wesley) de Raymond Chen, durante muchos años programador de la división de Windows de Microsoft.

[13]  Inmediatamente después del bucle de mensajes está la terminación del programa. Generalmente se devuelve un valor (todas las aplicaciones deberían hacerlo). En este caso el valor devuelto es el de uno de los miembros (wParam) del último mensaje recibido.

[14]  Esta es la definición del Windows procedure de nuestro programa [2]. Esta función recibe los mensajes no encolados del Sistema como resultado de la petición DispatchMessage(), o de cualquiera de las funciones que permiten enviar mensajes no encolados (send messages). Como puede verse, recibe como primer argumento un "handler" a la ventana destinataria; el segundo es un entero sin signo, indicativo del tipo de mensaje; los dos argumentos restantes contienen información complementaria sobre el mensaje recibido.

Generalmente el cuerpo de esta función está constituido por una larga sentencia switch ( 4.10.2) en la que se toman las decisiones correspondientes en función del mensaje recibido. En nuestro ejemplo se ha incluido la operatoria mínima necesaria para cerrar la aplicación.  Observe que los mensajes no procesados (distintos de los tipos WM_CLOSE y WM_DESTROY) son reexpedidos al Sistema mediante la función DefWindowProc, que es invocada con los mismos argumentos que se recibieron.  Es como decirle a Windows: "No se que hacer con este mensaje (o no me interesa), haz lo que estimes procedente al respecto".  Por el contrario, si nuestro Windows procedure se hace cargo de la totalidad del proceso correspondiente al mensaje recibido, no lo reexpedirá al DefWindowProc, y devolverá un 0 (falso).

Como su nombre indica, la función DefWindowProc invoca al denominado Default Windows Procedure; un mecanismo proporcionado por Windows a nuestra aplicación, para procesar todos los mensajes que no hayamos procesado específicamente en nuestro código (esto garantiza que todos los mensajes serán procesados adecuadamente). Es significativo que este es justamente el destino de la inmensa mayoría de los mensajes que recibe cualquier aplicación Windows. El funcionamiento normal supone un ingente tráfico de mensajes (se generan centenares con el solo acto pasear el ratón sobre una ventana), de los cuales, solo una ínfima parte son procesados por el código de la aplicación.

Es igualmente significativo que nuestro Windows procedure devuelve el valor devuelto por la invocación a  DefWindowProc, y que nuestro procedimiento Windows es invocado por el Sistema; nuestra aplicación no debe invocarlo directamente. Como hemos señalado, Windows proporciona mecanismos indirectos para hacerlo. Por ejemplo, la función CallWindowProc.  Una razón, entre otras, para que no invoquemos directamente nuestro Windows procedure, es que la mencionada CallWindowProc proporciona conversión automática de Unicode/ANSI; conversión que se perderá si realizamos la invocación directamente.

 Prev.