Mensajes del más allá: Windows Messages

A estas alturas de la vida hablar de programación con las APIs de Windows no es muy glamuroso, pero teniendo en cuenta que cada vez hay menos gente que haga programación de bajo nivel en windows, y lo que decíamos sobre la importancia de conocer las bases, no está de más recordar, aunque sea muy por encima, cómo funciona windows en lo que interfaz de usuario se refiere.

OJO: No soy un experto en este tema y lo único que pretendo es ofrecer una idea global. Si alguien quiere aprender de verdad sobre los entresijos de windows, lo mejor que puede hacer es seguir otros blogs de programación en windows.

Las bases: WinAPI

Muchos estamos acostumbrados a hacer aplicaciones de escritorio para windows usando lengajes de alto nivel (VB, Java, C#, VB.NET, etc.) que integran algún framework o librería para simplificar la creación el interfaz de usuario (Swing, WinForms, WPF, …). Incluso muchas veces sobre estos frameworks se monta una abstracción de mayor nivel para adecuarlos al diseño que vayamos a utilizar (MVP, MVVM, MVC, …) que nos facilita aún más las cosas (Caliburn, PRISM, etc.).

Sin embargo, todas estas plataformas acaban siendo implementadas sobre lo mismo: las eternas APIs de Windows (WinApi). Estas APIs nacieron hace muchos años, cuando la tierra era un lugar oscuro y los ordenadores empezaban a funcionar con Windows 1.0, y se basan en dos ideas principales:

  1. Todo es una ventana identificada por un handle (HWND).
  2. La comunicación entre ventanas se realiza a través de mensajes.

Al decir que todo es una ventana, me refiero a que cualquier elemento gráfico, ya sea una botón, un menú, un elemento de un menú, un cuadro de texto, una etiqueta, una barra de scroll, etc., se considera una ventana. Teniendo en cuenta esto, y como ya os imaginaréis, una ventana puede estar formada por otras ventanas. Esto se mapea bastante bien a las bases de los frameworks orientados a objetos. Por ejemplo, en WinForms, todo es un Control. De hecho, aunque estén desarrolladas en C, a su manera las APIs de Windows están orientadas a objetos. Cada ventana tiene un handle asociado que permite identíficarla unívocamente.

Que la comunicación entre ventana se realice con mensajes quiere decir exactamente eso, que existen un montón de mensajes definidos que pueden enviarse a una ventana para conseguir que haga cosas, desde mostrarse u ocultarse a cambiar el color de fondo o el texto que contiene.

Parte de estos mensajes serán enviados por las aplicaciones para cambiar su estado (por ejemplo, cuando se abra un documento, cambiar el texto de la barra de título de la ventana principal para reflejar el nombre del documento abierto), pero otros mensajes serán enviados por los periféricos que utiliza el usuario para interactuar con el ordenador: teclado y ratón.

En windows existe un componente, el gestor de ventanas (Window Manager) que, entre otras cosas, se encarga de hacer de mediador entre las distintas ventanas que aparecen en pantalla y los eventos externos que se generan. Básicamente, hace algo parecido a esto para decidir qué ventana debe recibir un mensaje:

Recibir evento externo
Si es evento de teclado,
    enviárselo a la ventana que tiene el foco
Si es evento de ratón,
    si la ventana tiene el foco
        enviarle el evento
    si no tiene el foco
        darle el foco

Vale, esta es una visión muy simplificada, pero para nuestro objetivo, nos sirve de sobra.

La recepción de mensajes: el Message Loop

Cuando windows crea un proceso con interfaz de usuario (vamos, al arrancar una aplicación), crea una cola de mensajes donde va almacenando los mensajes que se envían a la aplicación. Para procesar los mensajes, crea una hebra, la hebra primaria, que es la única hebra que puede procesar y enviar mensajes a las ventanas (a los que programáis en .NET, os sonará Control.InvokeRequired y SynchronizationContext).

Con esto, el esquema básico de una aplicación de windows es:

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

Esto es una versión simplificada de lo que se conoce como Message Loop, y es el núcleo de cualquier aplicación gráfica de windows. La idea es sencilla, mientras pueda leer mensajes, se hace un preproceso del mensaje con TranslateMessage (realmente, esto convierte mensajes de pulsación de tecla en mensajes de generación de caracteres) y se usa DispatchMessage para enviar el mensaje a la ventana que debe procesarlo.

El proceso de mensajes: WndProc

Cada ventana de la aplicación, y recordemos que todo es una ventana, tiene asociado lo un WndProc, que es una función que se encarga de procesar los mensajes de la forma adecuada. La pinta de un WndProc típico es algo así:

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_XXXX:
             ...
        break;
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

Es feo, sí, sobre todo porque el switch puede crecer hasta límites insospechados si no se hace con cuidado (de verdad, he visto código que jamás creeríais). Aun así, hay que reconocer que la idea es sencilla. La función recibe el hwnd que identifica la ventana, el tipo de mensaje (msg) y dos parámetros específicos del mensaje, wParam y lParam. Estos parámetros tienen distinto significado en función del mensaje al que están asociados. Por ejemplo, en un mensaje para mostrar una ventana, podríamos usarlo para indicar si la ventana debe ser mostrada u ocultada, o en un mensaje para cambiar el texto de una ventana, pueden ser un puntero al texto que queremos mostrar.

El caso por defecto del switch muestra claramente la orientación a objetos de WinAPI. Lo que estamos haciendo es llamar al WndProc por defecto de esa clase de ventana, es decir, estamos invocando a la clase base. Sería equivalente a escribir en C# base.WndProc(hwnd, msg, lParam, wParam). En realidad, esto es lo que permite que existan controles estándar en Windows. Hay clases de ventanas ya definidas con su WndProc asociado que se encarga de dibujarlas en pantalla, responder a los mensajes necesarios y demás.

Por ejemplo, el WndProc de un botón es capaz de convertir mensajes en crudo del ratón, como «botón izquierdo pulsado», «botón izquierdo soltado», en mensajes de un nivel un poco más alto: «click en el botón», enviando a su ventana padre un mensaje BM_CLICK.

¿Y a mi qué? Yo programo en .NET…

Ahora que tenemos una idea básica de cómo funciona windows, es más fácil explicar algunos comportamientos que al principio pueden parecer chocantes cuando programas en .NET (o lenguajes similares).

Por ejemplo, cuando en el EventHandler del Click de un botón decidimos hacer un proceso largo, como la descarga de un fichero de internet, y la ventana deja de redibujarse, es porque realmente estamos bloqueando el bucle de mensajes y no se puede procesar el mensaje que Windows le manda a la ventana para decirse que se repinte.

Cuando hacemos un Application.DoEvents(), en realidad lo que estamos haciendo es permitir que se ejecute el bucle de mensajes para procesar los mensajes pendientes.

Cuando usamos el Timer de System.Windows.Forms.Timer, en realidad lo que estamos haciendo es recibir mensajes WM_TIMER cada cierto tiempo en nuestra cola de mensajes, por lo que la precisión es bastante baja ya que el mensaje puede estar durante mucho tiempo en la cola esperando a que lo procesemos si estamos atendiendo otros procesos. Esto hace también que, por muchos timers que tengamos en un mismo Form, nunca puedan ejecutarse dos exactamente a la vez, ya que como hemos visto el proceso de los mensajes de la cola de mensajes es secuencial.

¿Por qué todo este rollo?

Esto sólo ha sido una introducción muy básica a como funciona el interfaz de usuario Windows. Antes, dominar esto hubiera sido vital para poder hacer cualquier aplicación, pero hoy en día esto queda tapado por capas y capas de software. En teoría eso nos hace más productivos (y en general así es también en la práctica), pero conocer los cimientos de las cosas es importante. Nunca se sabe cuando te va a tocar remangarte y, desde tu preciosa torre de marfil de Controls y Forms, reescribir un WndProc.

Un comentario en “Mensajes del más allá: Windows Messages

  1. Pingback: IoC vs ServiceLocator | Contexto

Comentarios cerrados.