El cambio en el paradigma de funcionamiento de un programa Windows y un programa C tradicional que sigue una secuencia de procedimientos establecidos de antemano en el programa fuente C es inevitable considerando que, a diferencia de lo que ocurre en un programa cuya interfaz visual con el usuario es una simple ventana de líneas de comandos DOS (en los tiempos en los que el Mouse aún no había sido inventado), en un entorno Windows puede haber varias maneras de usar un mismo programa, empezando por el hecho de que en un momento dado al usuario se le pueden dar varias opciones tales como el enviar un comando a través del teclado, o decidir un curso de acción a través del Mouse al grado de poder abrir una caja de opciones con un clic del botón derecho del Mouse, y hasta pasar de un programa Windows a otro programa Windows diferente dejando pendiente el primero hasta que se termine de usar el segundo (en un medio multitareas). Puesto de otra manera, mientras que en un programa C tradicional para una ventana de líneas de comandos lo que fija la secuencia seguida por el programa son las instrucciones puestas en el código fuente original por el programador, en un programa Windows lo que fija la secuencia de sucesos o eventos que habrán de ocurrir son las decisiones que vaya tomando el mismo usuario, una secuencia por completo impredecible porque depende de lo que esté planeando hacer cada usuario en particular. Y cuando ocurre un evento, cada evento en particular debe ser capaz de enviarle un mensaje al programa Windows ordenándole tomar cierta acción. Para cada evento posible en la ejecución de un programa Windows, debe de haber un mensaje que se le envíe a Windows (ya sea desde el teclado o desde el Mouse) indicándole al programa Windows lo que se debe de hacer.
Un programa elaborado para Windows nunca hace nada hasta que Windows no le haya enviado un mensaje.
Para entender lo que son los mensajes de Windows (y de hecho, los mensajes en cualquier entorno o sistema operativo visual tipo Windows como MacOS, Linux y Android), se vuelve necesario comprender una nueva táctica para la elaboración de programas, una nueva arquitectura de programación. Esto es lo que llamamos una programación manejada por eventos, conocida también como una programación impulsada por eventos. Los eventos son los que controlan todo en un programa Windows. Y aún cuando no se tenga abierto algún programa Windows (como Word, Excel, Photoshop, etcétera) sino simplemente se esté usando un sistema operativo Windows para examinar los contenidos de una carpeta o para copiar un archivo de un lugar a otro, todo se lleva a cabo mediante eventos.
Quizá la mejor manera de comprender la manera en la que funciona la programación manejada por eventos, y lo que son los mensajes, sea repasar un programa Windows que maneje mensajes que puedan ser generados directamente por el usuario. Esto lo haremos con el programa mostrado a continuación titulado EVENTOS.C:
// Programa EVENTOS.C // El objetivo es contar con un programa que sea capaz de // responder a mensajes del teclado y del Mouse #define STRICT // chequeo estricto de tipos; eliminar esta // linea en caso de que haya problemas para // la compilacion #include "windows .h=" // archivo include para los // programas Windows // prototipo del procedimiento WndProc() LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM); // Nombre de la aplicacion Windows PSTR szProgName = "Evento de opresion tecla/boton"; ///////////////////////////////////////////////// // WinMain() -- punto de entrada del programa // ///////////////////////////////////////////////// #pragma argsused // ignorar argumentos no usados int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow) { HWND hWnd; // agarradera (handle) a una ventana MSG msg; // mensaje de GetMessage WNDCLASS wndClass; // estructura de clase window if(!hPrevInst) // en caso de que sea la primera ventana { wndClass.style = NULL; // estilo predeterminado // domicilio en RAM de la funcion WndProc wndClass.lpfnWndProc = (WNDPROC)WndProc; wndClass.cbClsExtra = 0; // no datos de clase // extra wndClass.cbWndExtra = 0; // no datos de ventana // extra wndClass.hInstance = hInstance; // que programa? // cursor de flecha convencional wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); // icono convencional blanco wndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION); // no habra una barrea de menu en la ventana wndClass.lpszMenuName = NULL; // un fondo blanco para el interor de la ventana wndClass.hbrBackground = GetStockObject(WHITE_BRUSH); // nombre de la clase de ventana wndClass.lpszClassName = "keybuttClass"; // registrar la clase RegisterClass(&wndClass); } // finalizar if // crear la ventana hWnd = CreateWindow("keybuttClass", // nombre clase ventana szProgName, // titulo en la ventana WS_OVERLAPPEDWINDOW, // estilo de traslape CW_USEDEFAULT, // posicion x // predeterminada CW_USEDEFAULT, CW_USEDEFAULT, // posicion y // predeterminada CW_USEDEFAULT, // anchura // predeterminada NULL, // agarradera (handle) // del progenitor NULL, // agarradera (handle) // de menu hInstance, // cual programa? NULL); // no hay datos // iniciales ShowWindow(hWnd, nCmdShow); // mostrar la ventana // bucle de mensaje while( GetMessage(&msg,0,0,0) ) // obtener mensaje de Windows { TranslateMessage(&msg); // convertir (opresiones de teclas) DispatchMessage(&msg); // invocar procedimiento window } // regresar a Windows return msg.wParam; } // finalizar WinMain() ////////////////////////////////////////////////////////////////// // procedimiento de la ventana principal -- recibe mensajes // ////////////////////////////////////////////////////////////////// LRESULT CALLBACK _export WndProc(HWND hWnd, // agarradera de ventana UINT msg, // numero mensaje WPARAM wParam, // parametro word LPARAM lParam) // parametro long { char szString[100]; // para funcion wsprintf() switch(msg) // cual mensaje? { case WM_CHAR: // el usuario oprime una tecla // desde el teclado MessageBox(hWnd, "Tecla oprimida desde el teclado", szProgName, MB_OK | MB_ICONINFORMATION ); break; case WM_LBUTTONDOWN: // el usuario oprime el boton // izquierdo del Mouse wsprintf(szString, "Boton izquierdo oprimido en x=%d, y=%d", LOWORD(lParam), HIWORD(lParam) ); MessageBox(hWnd, szString, szProgName, MB_OK | MB_ICONINFORMATION ); break; case WM_DESTROY: // nosotros generamos este mensaje PostQuitMessage(0); // enviar WM_QUIT break; // todos los demas mensajes son manejados por el // procedimiento DefWindowProc() default: return( DefWindowProc(hWnd, msg, wParam, lParam) ); } // finalizar switch(msg) return 0L; // definicion de retorno si se manejo msg } // finalizar WndProc
Obsérvese que el listado de este programa Windows es casi idéntico al listado del programa VENTANA.C, excepto que este programa incluye dos enunciados case adicionales en la función-procedimiento WndProc().
Cuando se ejecuta el programa, la ventana Windows que aparece en la pantalla es la misma ventana que la ventana principal que es puesta por el programa VENTANA.C. Sin embargo, además de la ventana, la cual tiene por título en la barra del título “Evento de opresion tecla/boton”, cuando se oprime el botón izquierdo del Mouse aparece una Caja de Mensaje como la que se muestra a continuación:
En la Caja de Mensaje aparecen unas coordenadas. Las coordenadas mostradas indican la posición del puntero del Mouse en la pantalla, relativas a la ventana, cuando fue oprimido el botón del Mouse. Podemos ver cómo cambian las coordenadas conforme movemos el puntero del Mouse hacia otra posición y oprimimos el botón del Mouse.
Si en vez de oprimir el botón izquierdo del Mouse oprimimos una tecla en el teclado, entonces aparecerá otra Caja de Mensaje distinta como la que se muestra a continuación:
Haciendo clic en el botón OK de cualquiera de las cajas hace que desaparezcan. Obsérvese que, por el diseño del programa, el texto puesto en la barra del título de la ventana principal es el mismo que el que se pone en las Cajas de Mensaje. Ambos textos pueden ser, desde luego, diferentes, esto va por cuenta del programador que diseña su programa Windows. El programador debe experimentar un poco con este programa Windows para convencerse de la manera en la cual funciona. Obsérvese que los clics del Mouse no tienen ningún efecto en el programa a menos de que el puntero del Mouse esté posicionado dentro de la ventana principal del programa. Obsérvese también que las opresiones de teclas en el teclado no tienen efecto alguno a menos de que la barra del título aparezca de color azul y no esté “emblanquecida”. El color azul “fuerte” y no emblanquecido indica que la ventana tiene el enfoque del teclado y está a la espera de opresiones de teclas en el teclado. Si la ventana del programa Windows no tiene el enfoque, entonces otra ventana debe tener el enfoque y hacia ella van las opresiones de teclas en el teclado (normalmente se le dá a una ventana haciendo clic con el Mouse dentro de cualquier parte de la ventana).
Se ha afirmado arriba que Windows utiliza una arquitectura manejada por eventos. Para poder apreciar el por qué se requiere una nueva especie de arquitectura de programa, vale repasar la manera en la cual trabaja un programa tradicional MS-DOS. En un programa tradicional que funciona con una ventana de líneas de comandos, tanto el usuario como el programa van poniendo lo suyo en la pantalla, siguiéndose una secuencia como la siguiente:
Programa: | Dame tu edad |
Usuario: | 35 |
Programa: | Dame tu peso en kilogramos |
Usuario: | 84 |
Programa: | Oprime G para guardar los datos, y P para imprimirlos |
Usuario: | G |
Algunas aplicaciones (como el procesador de palabras WordStar para DOS) han usado una presentación más elaborada, como fondos de pantalla que muestran listas de opciones y otros refinamientos, pero el efecto final viene siendo el mismo. El programa de aplicación DOS espera hasta obtener una respuesta del usuario. Una vez que la obtiene, toma alguna acción, y le pide al usuario otra respuesta, esperando otra acción del usuario. Internamente, al estar esperando una respuesta del usuario, el programa se mantiene ocupado ejecutando en un bucle de espera, checando el teclado para ver si el usuario ha oprimido alguna tecla usando una función C como scanf() o getche(). Aún cuando el programa está esperando alguna entrada del usuario, el procesador CPU de la computadora sigue ejecutando instrucciones (las instrucciones del bucle). Por lo general, el programa monopoliza todo el tiempo del procesador CPU y la memoria, así como la mayoría de los demás recursos del sistema (por ejemplo, los discos duros).
En este escenario tradicional, el papel del sistema operativo se limita a encontrar y cargar en la memoria RAM el archivo de aplicación buscado, y echarlo a andar transfiriéndole el control de la máquina. Una vez que el programa de aplicación ha sido lanzado, el sistema operativo sale fuera del panorama hasta que termina la ejecución del programa. Cuando ocurre algún evento de entrada/salida, tal como el usuario oprimiendo una tecla del teclado, esta información no va al sistema operativo sino directamente al programa de aplicación DOS, como se muestra en la siguiente figura:
El modelo del programa tradicional funciona muy bien en una computadora en la que solo se ejecuta un programa a la vez, en donde suponemos que solo un programa de aplicación ha sido cargado en la memoria RAM y ese programa es el que se está ejecutando
El problema con Windows es que puede haber no uno sino varios programas cargados a la vez en la memoria RAM (cada programa ocupa su propio espacio en la memoria) compartiendo tiempo del procesador CPU, con cada programa representado por su propia ventana en la pantalla, lo cual representa una complejidad mayor. Nos preguntamos: en Windows, si una aplicación se está ejecutando y el usuario oprime una combinación de teclas de sistema como [ALT] y [Q], ¿quién se encarga de manejar ésta combinación de teclas? Si hay varias aplicaciones que se están ejecutando, y el usuario oprime cualquier tecla del teclado, ¿quién decide cuál es el programa que debe recibir esa opresión de tecla? Si el usuario hace clic con un botón del Mouse, ¿qué programa es afectado? Y si dos programas distintos quieren usar la impresora, ¿quién decide cuál de los dos programas debe recibir prioridad? Al reflexionar sobre estas preguntas, nos damos cuenta de que el modelo de programación tradicional MS-DOS no funcionará en Windows. Una aplicación no puede tomar el control de todos los recursos de entrada/salida y de todos los recursos del sistema, porque otras aplicaciones que se están ejecutando al mismo tiempo también requieren entrada del usuario y necesitan accesar otros dispositivos de entrada/salida. Se requiere de alguna especie de super-programa que lleve el control de los dispositivos de entrada/salida, decida cuál de las aplicaciones que se están ejecutando debe ser notificada acerca de un evento tal como una opresión de botón del Mouse, y pasarle a la aplicación los datos acerca del evento. Ese es el papel desempeñado por Windows.
Resulta que Windows es el responsable de todos los movimientos y los clics del Mouse, todas las opresiones de teclas en el teclado, todos los accesos a los discos duros y las impresoras, y todos los eventos relacionados con los dispositivos de entrada/salida. No puede haber un programa que por sí solo tome el control directo de un dispositivo de entrada/salida; si pudiera entonces otros programas no obtendrían la entrada destinada a ellos.
Cuando el usuario hace clic en el botón del Mouse, Windows determina en dónde está posicionado (en la pantalla) el cursor del Mouse, y transmite la información del clic del Mouse a la aplicación cuya ventana está directamente detrás del cursor. Otros programas, que pueden tener también ventanas en la pantalla, no son afectados. Cuando el usuario oprime una tecla del teclado, Windows determina cuál ventana tiene el enfoque del teclado, y le envía esa información a esa ventana y a ninguna otra. La siguiente figura nos muestra como maneja Windows los eventos:
Un sistema tradicional que opera con una ventana tradicional DOS de líneas de comandos cede el control de la máquina a una aplicación durante todo el tiempo que se ejecuta la aplicación. En contraste, Windows cede el control de una aplicación únicamente cuando ocurre un evento que afecta a la aplicación. Si un usuario oprime una tecla cuando la ventana de una aplicación tiene el enfoque, Windows le transmite esa información a la aplicación, y la aplicación toma la acción apropiada que puede ser la impresión de un caracter en la ventana. Una vez que la aplicación ha procesado la opresión de la tecla, el control es devuelto a Windows de modo tal que Windows puede checar la siguiente opresión de tecla o clic del Mouse, que puede estar dirigido o no a la misma aplicación. Cuando una aplicación no se está ejecutando, entonces Windows u otra aplicación se está ejecutando. Podemos imaginar del modo siguiente la distribución del tiempo del procesador CPU en un programa tradicional DOS y un programa Windows:
Es importante irse acostumbrando a la idea de que en Windows una aplicación toma control del procesador CPU únicamente por períodos breves de tiempo, y ello como resultado de algún evento externo.
¿Y cómo le dice Windows a un programa de aplicación Windows acerca de un evento que ha ocurrido, trátese de una opresión de tecla en el teclado o la opresión de un botón del Mouse o cualquier otra operación llevada a cabo a través del hardware? Lo hace enviando un mensaje. Un mensaje es como una pequeña unidad de información, algo así como una nota de boletinado o “posteado”. Cada vez que ocurre un evento que afecta a un programa de aplicación, Windows le envía un mensaje a la ventana de la aplicación. Un mensaje contiene información como “el usuario ha oprimido la tecla [H]” o como “el usuario ha oprimido el botón de Retry” o como “el usuario ha oprimido el botón derecho del Mouse” o “el usuario ha seleccionado la opción de menú Guardar”. ¿Y cuál es el mecanismo empleado para la transmisión de los mensajes? Windows le transmite un mensaje a una aplicación invocando una función hacia dicha aplicación. Este es uno de los conceptos fundamentales en la programación Windows. La transmisión de un mensaje significa la invocación de alguna función. ¿Y cuál función es invocada por Windows en una aplicación? Aquí surjen varias posibilidades. Puede haber una sola función encargada de recibir todos los mensajes enviados a una aplicación. Como también puede haber distintas funciones para distintos tipos de mensajes. Sin embargo, es un hecho que una ventana es la unidad básica de la organización de un programa en Windows. De este modo, existe una función para recepción de mensajes asociada con cada clase de ventana, lo cual puede representar un ahorro de espacio, ya que varias ventanas, siempre y cuando compartan la misma clase, pueden compartir la misma función para recibir mensajes.
En el programa EVENTOS.C que hemos visto arriba así como en el programa VENTANA.C que hemos visto previamente existe una clase de ventana, una ventana derivada de dicha clase, y una función para recepción de mensajes llamada WndProc(). Tal función encargada de recibir mensajes es conocida como un procedimiento de ventana Window (a veces es llamada también una función de ventana). La siguiente figura ilustra la manera en la cual los mensajes son enviados a los procedimientos de ventana:
Así pues, Windows invoca un procedimiento (o función) Windows para transmitir un mensaje. La invocación en sí dice “Aquí tengo un mensaje”. ¿Pero cómo puede saber el procedimiento Window WndProc() el significado del mensaje? ¿Cómo puede distinguir una notificación de una opresión de tecla en el teclado de una opresión de un botón del Mouse, o de una selección de Menú, o de cualquier otra cosa? Como puede suponerse, Windows le pasa al procedimiento la información que constituye el mensaje a través de los parámetros del procedimiento. Esto resulta obvio cuando vemos el declarador de la función WndProc():
LRESULT CALLBACK _export WndProc(HWND hWnd, // agarradera de ventana UINT msg, // numero mensaje WPARAM wParam, // parametro word LPARAM lParam) // parametro long
Hay cuatro parámetros para el procedimiento Window WndProc(). El primer parámetro es la agarradera (handle) de la ventana en particular que recibe el mensaje. Esta es la misma agarradera de ventana de tipo HWND que es regresada por la función API CreateWindow() que aparece en la función principal WinMain(). Esta agarradera de ventana es usada como argumento hacia otras funciones, así que resulta conveniente pasarla al procedimiento de ventana. El segundo parámetro para el procedimiento WndProc() es el número del mensaje, un número entero positivo. representado en el programa EVENTOS.C con la variable msg. El número de mensaje es una variable importante en un programa Windows, puesto que todo el procedimiento de ventana está organizado en torno a dicho número. En el procedimiento WndProc() un enunciado de control switch ocasiona que se tomen distintas acciones dependiendo del valor del número del mensaje. La siguiente figura muestra el aspecto de las transacciones:
Obsérvese el papel desempeñado por constantes como WM_CHAR, WM_LBUTTONDOWN y WM_DESTROY, las cuales son usadas para codificar distintos valores de números de mensajes. Estas constantes aparecen definidas en el archivo de cabecera WINDOWS.H, y son apenas unas cuantas de otras constantes como las que se muestran en la siguiente tabla (el prefijo “WM” afijado a todas las constantes significa “Window Message”):
Constante | Significado |
WM_CHAR | El usuario oprimió una tecla del teclado |
WM_CLOSE | La ventana será cerrada |
WM_COMMAND | El usuario escogió algo del menú |
WM_CREATE | Se está creando una ventana |
WM_DESTROY | Se está eliminando una ventana |
WM_LBUTTONDOWN | El usuario oprimió el botón izquierdo del Mouse |
WM_RBUTTONDOWN | El usuario oprimió el botón derecho del Mouse |
WM_MOVE | Una ventana fue movida de lugar |
WM_PAINT | Hay que dibujar una ventana de nuevo |
WM_SIZE | El tamaño de la ventana fue cambiado |
Cuando hizo su aparición Windows 3.1, había ya más de cien mensajes orientados a Windows, y la lista ha ido creciendo con cada versión nueva de Windows conforme han estado apareciendo nuevas herramientas del hardware como las pantallas táctiles y los posicionadores táctiles de cursor tipo “touchpad” (que hacen posible prescindir del Mouse sobre todo en computadoras portátiles laptop y tabletas electrónicas). Afortunadamente, no todos los mensajes son críticos, y es posible desarrollar programas Windows con una lista básica de mensajes que se cuentan entre los cientos y no los miles.
Muchos mensajes incluyen información adicional además del simple número de mensaje. Los últimos dos parámetros al procedimiento Window, wParam y lParam, son usados para transmitir esta información adicional. Bajo Windows 3.1, el tipo derivado WPARAM era unsigned int (dos bytes asignados), y el tipo derivado LPARAM era de tipo LONG (cuatro bytes asignados).
¿Qué es lo que involucra el enviar desde Windows un mensaje a un programa Windows? Cuando se está enviando un mensaje, de hecho lo que se está enviando es toda una estructura, una estructura definida al estilo de como se define en el lenguaje C, la cual contiene varios campos de información. Al recibir el programa de aplicación Windows un mensaje, lo que tiene que hacer es procesar toda una estructura, lo cual involucra el tener que procesar cada campo de la estructura. La estructura fundamental MSG que apareció con Windows 3.1 para definir los mensajes se encuentra definida en el archivo de cabecera WINDOWS.H del modo siguiente:
typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG;
La siguiente ilustración muestra la misma información contenida en la definición de la estructura MSG al ser enviada como un paquete completo (el campo “pt” significa “point”):
hWnd | message | wParam | lParam | time | pt |
El campo “message” de la estructura MSG codifica el tipo de mensaje, que puede ser fijado a uno de varios valores posibles tales como:
message = WM_PAINT
message = WM_COMMAND
message = WM_CREATE
message = WM_DESTROY
La variable wParam es del tipo derivado WPARAM, mientras que la variable lParam es del tipo LPARAM, lo cual facilita la memorización de estas características.
Para mensajes que usan el campo wParam, este es un parámetro con un tamaño WORD (unsigned short), tales como WM_KEYDOWN o WM_COMMAND. Por otro lado, para mensajes que usan el campo lParam, este es un parámetro con un tamaño LONG, como el mensaje WM_MOUSEMOVE.
La información específica transmitida en wParam y lParam depende del mensaje. En el programa Windows EVENTOS.C usamos la información regresada en lParam, cuando se recibe un mensaje WM_LBUTTONDOWN, para calcular las coordenadas del puntero del Mouse. La coordenada-X se encuentra especificada en la palabra baja (byte bajo) de este parámetro, y la coordenada-Y se encuentra especificada en la palabra alta (byte alto). Usamos los macros LOWORD y HIWORD (introducidos en la entrada “La programación Windows II”) para extraer dichos valores para ser usados posteriormente por la función wsprintf() de modo tal que puedan ser puestos en la Caja de Mensaje.
En el programa VENTANA.C que vimos en la entrada anterior, el procedimiento WndProc() manejaba únicamente un solo mensaje: WM_DESTROY. Este mensaje es también manejado en el programa EVENTOS.C. De hecho, todas las aplicaciones Windows deben manejar el mensaje WM_DESTROY, es el único mensaje que no es optativo. Dicho mensaje es enviado por Windows cuando una ventana ha sido removida de la pantalla, es el mensaje que se usa para cerrar las aplicaciones Windows. Una manera en la que es manejado este mensaje es cuando el usuario selecciona la opción Cerrar (Close) del menú del sistema. Cuando un procedimiento Window WndProc() recibe este mensaje, tiene que ejecutar la función PostQuitMessage() usándose un solo argumento de cero (0). Esta función a su vez (como una caída secuencial de fichas de dominó que están alineadas paradas una tras otra, cuando una de ellas inicia la caída en cascada) ocasiona que se transmita un mensaje WM_QUIT a la aplicación, lo cual hace que termine la ejecución del programa Windows. ¿Y qué sucede cuando no se incluye la función PostQuitMessage()? En una situación así, desaparecerá la ventana de la aplicación, pero el programa seguirá ejecutándose por siempre hasta que la computadora sea apagada, sin poder romper su salida del bucle de mensaje.
Cuando una aplicación Windows ejecuta el enunciado return en un procedimiento como WndProc() en el programa EVENTOS.C, el control de la máquina le es regresado a Windows. Windows a su vez le puede ceder el control de la máquina a otra aplicación si tal cosa se requiere, o puede estar a la espera de que ocurra otro evento. Esta es una de las dos maneras en las que una aplicación transfiere el control del procesador CPU a Windows. En virtud de que el Windows ordinario (como Windows 3.1 y Windows 95) es un sistema operativo que depende de que la misma aplicación Windows le esté regresando periódicamente el control de la máquina a Windows de modo tal que la ejecución de la aplicación Windows no pueda ser interrumptida y “sacada fuera” por el mismo sistema operativo bajo un modelo definido como modelo no-apropiativo (la palabra inglesa es non-preemptive), el programa Windows le tiene que estar regresando frecuentemente el control de la máquina a Windows (de preferencia después de un lapso muy breve de tiempo) porque de otro modo otras aplicaciones que estén abiertas no tendrán oportunidad de seguirse ejecutando. En contraste, en sistemas operativos como Windows NT y OS/2, que son apropiativos (preemptive), cada uno de ellos puede interrumpir por cuenta propia una aplicación Windows en cualquier momento cuando el sistema operativo así lo requiera, y de este modo el programa Windows no tiene que preocuparse de tener que actuar con rapidez para estarle regresando a Windows el control del sistema.
Un engranaje fundamental de un programa Windows radica en el bucle de mensaje que para el programa EVENTOS.C es el siguiente:
while( GetMessage(&msg,0,0,0) ) // obtener mensaje de Windows { TranslateMessage(&msg); // convertir (opresiones de tecla) DispatchMessage(&msg); // invocar procedimiento window }
Para poder escribir programas Windows no es absolutamente indispensable entender a fondo la manera en la cual funciona este código o inclusive el por qué es necesario, todo lo que es necesario es tomar el código tal cual e injertarlo en el programa para que lleve a cabo su labor. De cualquier modo, a continuación se dará una idea de la manera en la que funciona el bucle de mensaje.
Resulta que hay dos maneras posibles de transmitirle un mensaje a un procedimiento Window WndProc(): el mensaje puede ser enviado, o puede ser “posteado” (boletinado). De cualquier modo, el resultado es que el procedimiento WndProc() es invocado con el número de mensaje así como otros datos como argumentos (cuando los haya). Sin embargo, la trayectoria que toma el mensaje en su ruta hacia el procedimiento WndProc() es más involucrada cuando es posteado que cuando es simplemente enviado.
El envío de un mensaje es algo así como hacer una llamada telefónica. Se envía un mensaje cuando es importante que llegue de inmediato al procedimiento WndProc(), y un ejemplo de ello es el mensaje WM_DESTROY. Sin importar lo que esté sucediendo, el procedimiento Window WndProc() tiene que procesar de inmediato el mensaje recibido. Para enviar un mensaje, Windows utiliza la ruta directa, hace una invocación al procedimiento del programa Windows. La mayoría de los mensajes son enviados, en lugar de ser posteados. La siguiente figura ilustra la ruta tomada cuando se envía un mensaje:
En contraste, postear un mensaje es como enviar una carta. Un mensaje es posteado cuando puede ser parte de una serie de mensajes que tienen que ser procesados secuencialmente uno tras otro siguiendo un orden fijo. Un ejemplo de ello es el mensaje WM_CHAR, que es posteado cuando una opresión de tecla en el teclado genera un caracter. Para evitar que los resultados de las opresiones de tecla se salgan fuera de orden revolviendo las letras que forman las palabras, Windows pone los mensajes WM_CHAR en una cola de espera (conocida en la literatura técnica como queue, pronunciado “qiu”). Una cola es simplemente una memoria buffer temporal que trabaja en un modo FIFO (First-In, First-Out, lo primero que entra es lo primero que sale), como las líneas de espera que se forman en una ventanilla de trámites. El programa Windows va removiendo estos mensajes de la cola cuando tiene tiempo, al igual que como ocurre cuando se sacan las cartas de un buzón postal cuando ello sea conveniente. La siguiente figura ilustra la ruta tomada por un mensaje cuando es posteado:
Cuando un mensaje es enviado, el programa Windows no tiene que tomar ninguna acción para recibir el mensaje, su procedimiento Window WndProc() simplemente está a la espera de ser llamado por el mensaje. Pero cuando el mensaje es posteado, el programa Windows tiene que trabajar un poco más duro. Tiene que remover el mensaje de la cola de espera para enviarlo a su propio procedimiento Window. ¿Y cómo saca un programa Windows un mensaje de la cola de espera para transmitirlo a su propio procedimiento WndProc()? Aquí es donde entra el bucle de mensaje. El bucle de mensaje es un bucle C del tipo while con dos funciones importantes del Windows API: GetMessage() y DispatchMessage(). El programa Windows ejecuta primero GetMessage() para decirle a Windows que no está procesando ningún mensaje, y que está listo para sacar el siguiente mensaje que se encuentre disponible en la cola de espera. Si la cola de espera está vacía, esta función no regresa nada, y Windows simplemente retiene el control. Esta es la segunda manera en la cual una aplicación le puede transferir a Windows el control de la máquina, para permitir de este modo que otras aplicaciones puedan ser ejecutadas. Windows permite que la función GetMessage() regrese a la aplicación cuando ocurre un evento que ocasiona que un mensaje sea posteado, tal como la opresión de una tecla del teclado. La información regresada por la función GetMessage() acerca de este evento es almacenada en una estructura C del tipo MSG descrita arriba, que en nuestro programa es la variable msg.
En cuanto la función GetMessage() regresa algo, el programa Windows ejecuta de inmediato la función DispatchMessage(). Esta función hace que Windows invoque el procedimiento WndProc() del programa Windows con los contenidos del mensaje, tomado de la estructura msg. La operación se repite una y otra vez, con GetMessage() obteniendo un mensaje de Windows, y DispatchMessage() pasándoselo al procedimiento Window WndProc().
Como puede apreciarse en el listado del programa EVENTOS.C, hay una tercera función en el bucle, la función API TranslateMessage(). La inclusión de esta función en el bucle es necesaria porque una opresión de tecla en el teclado no genera un mensaje WM_CHAR (que reporta un código de caracter), sino un mensaje de bajo nivel que indica cierta tecla en particular. TranslateMessage() traduce estos mensajes de bajo nivel en un mensaje WM_CHAR, que es lo más conveniente para los programas Windows.
En la elaboración de programas Windows, usualmente no nos preocupamos mucho acerca del bucle de mensaje, no tenemos que recordar cuáles mensajes son enviados y cuáles mensajes son posteados, ya que Windows se encarga de los detalles en la transmisión de los mensajes. Tampoco tenemos que accesar la estructura MSG (lo cual se puede hacer con el operador punto) y ni siquiera tenemos que recordar los contenidos de dicha estructura. Lo principal es que un mensaje que es posteado eventualmente termina siendo manejado por un procedimiento Window al igual que ocurre con un mensaje que ha sido enviado. De lo que sí tenemos que preocuparnos es de la manera en la que son manejados los mensajes en el procedimiento WndProc().
El bucle de mensaje permanece ciclando mientras la aplicación Windows se esté ejecutando (y en rigor de verdad, la mayor parte del tiempo la ejecución tanto de Windows como de los programas que corren bajo Windows se consume en la ejecución de bucles). ¿Pero cómo podemos romper el bucle perpetuo saliendo del mismo? Como podemos verlo en el listado, el bucle terminará si la función GetMessage() regresa un valor de cero (0):
while( GetMessage(&msg,0,0,0) ) { ... }
Solo hay una manera en la cual la función GetMessage() regresa un valor de cero, y esto ocurre cuando el mensaje que recibe de Windows es WM_QUIT. ¿Y qué es lo que ocasiona dicho mensaje? La misma aplicación Windows se encarga de lo que ocurre a resultas del envío del mensaje WM_QUIT, ejecutando la función PostQuitMessage(). Esto lo hace cuando recibe un mensaje WM_DESTROY, el cual es enviado cuando, por ejemplo, el usuario hace clic con el Mouse en la opción Cerrar (Close) del menú del sistema. Cuando el programa Windows ejecuta la función PostQuitMessage(), Windows postea el mensaje WM_QUIT. La siguiente figura ilustra el proceso que se lleva a cabo:
El mensaje WM_DESTROY le dice al procedimiento WndProc(): “Debes encargarte de hacer lo que sea necesario para terminarte a tí mismo”, y WM_QUIT le dice a la función principal WinMain(): “Estás siendo terminada”. Una vez que se ha salido del bucle de mensaje, la aplicación le devuelve a Windows el control de la máquina, usando parte de los datos obtenidos por la función GetMessage() del mensaje WM_QUIT como un valor de retorno (Windows utiliza este valor para procesamiento interno propio de Windows).
A estas alturas parece lógico preguntarse: ¿por qué los diseñistas de Windows no incrustaron el bucle de mensaje dentro del mismo Windows, en lugar de hacer que cada programa Windows contenga su propio bucle de mensaje? En la mayoría de las situaciones, tal cosa puede funcionar bien, pero algunos programas tienen que examinar mensajes posteados antes de que sean enviados al procedimiento Window WndProc(), y la forma de lograr que esto sea posible es poniendo el bucle de mensaje en la aplicación. La operación del bucle de mensaje y la manera en la que los programas Windows llegan a su conclusión puede parecer algo rebuscado, pero a fin de cuentas el programador no tiene por qué preocuparse acerca de estas cosas, ya que el bucle de mensaje funciona de manera automática. El programador lo único que tiene que hacer es incrustar el bucle de mensaje en el lugar apropiado dentro de la función principal WinMain(), dejándolo hacer lo que tenga que hacer.
Falta repasar otra función del Windows API que aún no hemos cubierto y que hemos usado en los programas VENTANA.C y EVENTOS.C. Se trata de la función DefWindowProc() (Default Window Procedure, procedimiento Window predeterminado), la cual juega un papel importante en los programas Windows. Para poder apreciar la importancia de esta función, basta preguntarnos a nosotros mismos: ¿qué sucede con los mensajes que no manejamos en el procedimiento de la aplicación? En el programa VENTANA.C, el único mensaje que manejamos es WM_DESTROY, y en el programa EVENTOS.C manejamos únicamente tres mensajes. Supóngase que Windows invoca al procedimiento WndProc() con algún otro mensaje, por ejemplo WM_COMMAND. ¿Lo pasamos por alto y lo echamos fuera?
La realidad es que un procedimiento Window como WndProc() tiene que tomar algún tipo de acción sobre cada mensaje que le sea enviado. Al madurar Windows 3.1, había ya más de cien mensajes disponibles, con cientos de mensajes adicionales que fueron agregados posteriormente en las siguientes versiones de Windows, y esto haría que cualquier programa, hasta el más sencillo, terminara resultando largo y complicado si hubiese la necesidad de tener que escribir un enunciado C case para manejar cualquier mensaje posible. Para poder mantener el programa Windows típico pequeño (relativamente hablando) y manejable, Windows proporciona una manera de esquivar la responsabilidad de tener que manejar aquellos mensajes sobre los cuales la aplicación no toma ningún tipo de acción. El programa Windows logra esto invocando la función DefWindowProc(). Esta función toma una acción predeterminada (default) para cualquier mensaje que le sea dado como parámetro.
En el programa EVENTOS.C, al igual que en la mayoría de los programas Windows, DefWindowProc() es instalado en el procedimiento Window WndProc() después de la palabra clave C default en el enunciado switch:
default: return( DefWindowProc(hWnd, msg, wParam, lParam) );
No es necesario que el programador esté al tanto de los detalles de lo que ocurre en cada procesamiento predeterminado cuando DefWindowProc() actúa sobre cada mensaje, lo único de lo que debemos preocuparnos es de instalarlo apropiadamente en el procedimiento Window:
Otra cosa en la que aún no hemos entrado en detalles es el valor de retorno proporcionado por el procedimiento Window WndProc(). Repasemos el declarador del prototipo para dicha función:
// prototipo del procedimiento WndProc() LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT es un tipo derivado, definido en el archivo de cabecera WINDOWS.H como de tipo long, mientras que CALLBACK apareció definido como _far _pascal, de modo tal que el procedimiento WndProc() tiene un domicilio de memoria RAM _far, es invocado con la convención de invocación Pascal, y regresa un tipo long. Las funciones que son invocadas por el mismo Windows son llamadas frecuentemente en la literatura técnica funciones callback invocando un concepto usado en las llamadas telefónicas, y el tipo CALLBACK usado en informática refleja esta nomenclatura. Por último, la palabra reservada _export es usada por el enlazador (linker), e indica que la función tiene que ser visible a otras aplicaciones y no solo a las funciones que se encuentran dentro de la misma aplicación Windows. La idea es parecida al uso de la palabra reservada extern usada en la construcción de proyectos con programas C tradicionales subdivididos en varios archivos y en los cuales se requiere hacer del conocimiento de un archivo de que la definición de algo que puede ser una variable externa o un archivo externo se encuentra “en alguno de los otros archivos de los que consta el proyecto”. No es lo mismo, desde luego, pero el concepto general es similar.
La programación manejada por eventos usada en Windows representa un cambio mayúsculo en relación a la programación procedimental usada en los programas MS-DOS implementados en ventanas de líneas de comandos, y se requiere de algún tiempo para familiarizarse y sentirse cómodo con el concepto. Al estar sentados frente a una computadora, la mayor parte del tiempo cuando no estamos interactuando con la computadora moviendo el Mouse u oprimiendo alguna tecla o tocando la pantalla táctil la aplicación simplemente permanece “sentada” sin hacer nada. La aplicación puede estarse “ejecutando” en el sentido de que ocupa un espacio en la memoria RAM y aparece listada en la lista de tareas pendientes de Windows (Windows Task List), pero ninguna de las instrucciones de la aplicación está siendo ejecutada ni está usando en lo absoluto tiempo del procesador CPU. Lo único que se está ejecutando es Windows, a menos de que le haya cedido temporalmente el control de la máquina a otra aplicación. El programa Windows despierta y recibe una oportunidad para entrar en acción cuando ocurre un evento que ocasiona que un mensaje sea transmitido ya sea enviado o posteado, tal como un clic del Mouse o la opresión de una tecla en el teclado cuando la aplicación tiene el enfoque del teclado, o cuando el usuario selecciona la opción Cerrar del menú del sistema. Para transmitir un mensaje a un programa, Windows invoca el procedimiento que hay en el programa. Los parámetros enviados a este procedimiento indican el tipo de mensaje así como los datos pertinentes. El procedimiento Window (que tiene un nombre como WndProc()) utiliza un enunciado de control switch para proceder de acuerdo al tipo de mensaje enviado, tomando acción apropiada. Cuando concluye la acción, el procedimiento Window le devuelve a Windows el control de la máquina. Y cuando ocurre otro evento, Windows invoca nuevamente el procedimiento Window con otro mensaje, repitiéndose la secuencia de eventos hasta que el usuario decide cerrar el programa. Al nivel del sistema operativo, o sea al nivel del mismo Windows, todo se sigue llevando a cabo mediante mensajes, hasta que el usuario decide apagar la máquina llevando a cabo el procedimiento de apagado con los eventos necesarios (clics del Mouse, opciones de barra de menú, etcétera) para que de este modo termine la sesión.