Inicio Reflexiones Guitarra Engineering Programación Win32

Formalizando ideas
Capítulo 5, por Ricardo González Garza


Este capítulo supone que tienes conocimeintos previos en. . .
Manejo del entorno de desarrollo
- Como Dev-Cpp, VC.NET, etc.
C++
- ¿Cómo funciona... ? switch(), break
- Conocer la librería <sstream>
Haber leído el Capítulo 3
- ¿Qué es... ? WinMain(), MessageBox(), CreateWindowEx(), ShowWindow()

Tiempo estimado de lectura y práctica: 45 min.

5.1 Mentalidad del programador en Windows

Sin tomar en cuenta el aspecto gráfico de interfaz de usuario, los programas de consola son relativamente más fácil de programar qué los de Windows. En un programa de consola el usuario debe seguir instrucciones específicas donde se le dicta qué hacer, mientras que los programas de Windows las acciones del usuario pueden ocurrir en distinto orden.

Por ejemplo, el ejecutable en consola dice "Dame tu nombre" y lo único que se puede hacer es introducir el nombre desde el teclado, si se quiere avanzar para introducir otra información primero debe teclear el nombre. En este caso, el programador solo necesita pensar en una secuencia de preguntas y esperar a que el usuario responda para seguir avanzando.

Llenar un formulario con los programas Windows es muy diferente. No hay reglas que el usuario deba seguir. Desde que inicia una aplicación en Windows, puede empezar a mover el puntero del ratón, quizás pulse algunos botones, maximice la ventana, introduzca información desde el teclado para llenar el campo de Dirección y después el Nombre... pero no está obligado a hacerlo de esa manera ni en ese orden. En este caso, el programador debe cambiar su metodología de programación, para responder las acciones del usuario sin importar la seccuencia.

¿Se pueden predecir las acciones del usuario en un programa Windows? No directamente (aunque se pueden restringir). Una aplicación en Windows responde a las acciones impredecibles y no secuenciales del usuario, es decir, espera una acción y emite una reacción.

5.2 Programación orientada a Eventos

Las acciones del usuario son interpretadas como eventos. Los programas que se escriben para Windows reaccionan a eventos enviados a la ventana del programa. Cada evento que ocurre se almacena automáticamente en una lista llamada Cola de Mensajes creada para cada aplicación.

Los eventos pueden ser acciones que realiza el usuario como mover el ratón, presionar un botón, escribir un texto, maximizar la ventana, etc. Otros eventos pueden ser los generados por el sistema como volver a trazar la ventana, mostrarla, quitar el foco, llegada de información por el puerto, etc.

fig. 5.2.1 Los eventos que recibe un programa Windows

Cada que sucede un evento se crea un mensaje y se pone en la cola del programa al que corresponde. El programa debe consultar la Cola de Mensajes (donde se almacenan estos eventos) y los recibe en forma de mensaje. Si la acción que efectua el usuario es de interés para el programa, éste reacciona con un código; de lo contrario, el programa lo ignora y lo manda a un procedimiento por omisión de Windows para que el sistema decida que hacer con el mensaje.

fig. 5.2.2 Manejo de mensajes en Windows

5.3 Bucle de mensajes

Pensado en términos de programación, debería existir un bucle que itere constantemente para verificar si existen nuevos mensajes. Si el mensaje significa terminar la ejecución (generado cuando el usuario oprime el botón cerrar por ejemplo) se sale del bucle y finaliza el programa (al salir de WinMain()).

Dado que Windows es un sistema multi-tarea, este bucle de mensajes no afectará el rendimiento de otros programas en ejecución (por lo menos no lo hará significativamente) ya que Windows se encarga de repartir equitativamente el uso del procesador a cada una de las aplicaciones.

Este bucle de mensajes es un componente básico de todas las aplicaciones de Windows.

Después de mostrar la ventana, la aplicación está preparada para llevar a cabo su tarea principal: procesar mensajes. Recuerde que Windows no envía información de entrada directamente desde el teclado o el ratón a una aplicación, sino que la ubica directamente en la cola de mensajes de la aplicación. La cola puede contener mensajes generados por Windows o mensajes enviados por otras aplicaciones. Observe el código siguiente:

Bucle de mensajes de una aplicación típica de WindowsCopiar cógido
    //Necesario para obtener los mensajes o eventos de la aplicación
    MSG mensaje;
    while(GetMessage(&mensaje, 0, 0, 0) == TRUE)
    {
        DispatchMessage(&mensaje);
    }

Una aplicación inicia con la llamada a la función WinMain(), en donde pueden inicializarse variables, crear ventanas, pero necesariamente requiere de un bucle mensajes como el anterior si se quiere mantener en ejecución al programa. Al salir de la función WinMain(), termina la ejecución del programa.

El código anterior es muy utilizado para procesar los mensajes que recibe la vantana (un bucle while), funciona en la gran mayoría de los casos, sin embargo no considera que GetMessage() puede regresar un error como veremos en el código más adelante.

La función GetMessage() consulta la cola de mensajes. Regresará el valor TRUE en caso de existir mensajes, lo cual permitirá ejecutar el código dentro de while para despachar el mensaje a su respectivo Procedimiento de Ventana.

La función GetMessage() copia el mensaje en la estructura MSG (puntero &mensaje) indicada en el primer parámetro. El resto de los parámetros normalmente se indican con 0, a menos que se requieran características especiales. El segundo parámetro 0 (o NULL) indica a la función que recupere los mensajes de cualquier ventana que pertenezca a la aplicación. Los dos últimos parámetros, 0 y 0, indican a GetMessage() que no aplique ningún filtro de mensaje. Los filtros de mensaje pueden restringir los mensajes recuperados a categorías específicas como pulsaciones o movimientos del ratón.

Hay dos condiciones que deben finalizar el bucle. GetMessage() devuelve FALSE (0) cuando el mensaje para procesar es WM_QUIT que significa salir de la aplicación (ej. cuando el usuario oprime el botón cerrar). Otra condición es cuando GetMessage() devuelve -1 indicando que ha ocurrido un error interno (como manipulador de ventana inválido), puede ocurrir cuando el sistema operativo se vuelve inestable. En base a esto, un bucle de mensajes más robusto será:

Un bucle de mensaje más elaboradoCopiar cógido
   BOOL bRet;

   while( (bRet = GetMessage(&mensaje, NULL, 0, 0)) != 0)
   {
      if (bRet == -1)
      {
         // GetMessage() regresó -1, es decir, ocurrió un error.
         // Lidiar con el error mostrando un mensaje con la
         // información obtenida usando GetLastError()
         // y MessageBox(), y terminar la aplicación
      }
      else
      {
         DispatchMessage(&mensaje);
      }
   }

La condición de while nunca debiera ser while(GetMessage(&mensaje, 0, 0, 0)), ya que aunque nuestra aplicación funcione correctamente mientras la probamos, esta condición seguirá introduciéndose al bucle sin finalizar la aplicación cuando el sistema se vuelva inestable y GetMessage() regrese el valor de -1.

Bucle de mensajes simple y seguroCopiar cógido
    MSG mensaje;
    while(GetMessage(&mensaje, 0, 0, 0) > 0)
    {
        DispatchMessage(&mensaje);
    }

La función DispatchMessage() es indispensable, ya que envía los mensajes actuales a los procedimientos de la ventana adecuada encargados de procesarlos. Ese procedimiento de ventana corresponde al indicado en el campo lpfnWndProc de la estructura WNDCLASSEX, en el capítulo anterior se enrutaba a la función ProcedimientoVentana() con el código ventana.lpfnWndProc = ProcedimientoVentana;.

5.4 Procesando los mensajes de ventana

Todas las aplicaciones de Windows necesitan una función WinMain(). Es el punto de inicio y final de la ejecución de un programa. La función WinMain() es responsable de los siguiente:

  1. Realizar cualquier inicialización requerida.
  2. Registrar el tipo de clase window de la aplicación.
  3. Contener el bucle de procesamiento de mensajes de la aplicación (visitar la cola de mensajes)
  4. Finalizar el programa, normalmente al salir del bucle de procesamiento de mensajes

Como se vio en el capítulo anterior, dentro de la función WinMain() se registra la estructura WNDCLASSEX de la aplicación. Esta clase de ventana WNDCLASSEX contiene campos que establecen el comportamiento de la ventana, íconos, el menú, la función que procesará los eventos, etc.

Para definir la estructura clase de ventana, la aplicación debe definir la variable de la siguiente manera: WNDCLASSEX ventana;. El campo que interesa en este momento es el que indica el procedimiento de ventana de la aplicación al que enviará los mensajes la función DispatchMessage(). Este campo se ha indicado anteriormente como ventana.lpfnWndProc = ProcedimientoVentana; y ahora explicaré el porqué.

El campo lpfnWndProc asigna la dirección de puntero de la función que procesará los eventos o mensajes de ventana. Este campo es del tipo WNDPROC, lo que significa que deberá indicarse un puntero a una función de 4 parámetros (HWND, UINT, WPARAM, LPARAM) y que devuelva LRESULT.

Asignación del campo lpfnWndProcCopiar cógido
   //Definición de WNDPROC declarada dentro de windows.h
   typedef LRESULT (*WNDPROC)(HWND, UINT, WPARAM, LPARAM);

   //Prototipo de la función de procedimiento de ventana
   //Ésta será la encargada de recibir los mensajes/eventos generados
   LRESULT CALLBACK ProcedimientoVentana(HWND hwnd, UINT mensaje,
                                         WPARAM wParam, LPARAM lParam);

   ...

   WNDCLASSEX ventana;
   ...
   ventana.lpfnWndProc = ProcedimientoVentana;
   ...

Windows dispone de cientos de mensajes diferentes que puede enviar a la función procedimiento de ventana. Estos mensajes tienen etiquetas con identificadores que comienzan por "WM_". Por ejemplo WM_COMMAND, WM_DESTROY, WM_INITDIALOG y WM_PAINT, etcétera. Estos identificadores también se conocen como constantes simbólicas, recordando que al final de cuentas son simples números con significado.

Procedimiento de ventanaCopiar cógido
#include <windows.h>

/* La función ProcedimientoVentana() es necesaria porque es la
   encargada de recibir los eventos (movimientos del ratón,
   tecla, clics a un botón, etc). En este caso, solo monitorea
   el momento el que el usuario decide cerrar la ventana para
   descargar la aplicación de memoria                           */
LRESULT CALLBACK ProcedimientoVentana(HWND hwnd, UINT mensaje,
                                      WPARAM wParam, LPARAM lParam)
{
    switch (mensaje)
    {
       case WM_DESTROY:
           PostQuitMessage (0);  //Publica el mensaje WM_QUIT en la cola de
                                 //mensajes que en la próxima iteracción del
                                 //bucle de mensajes hará que GetMessage()
                                 //regrese el valor 0 y por lo tanto salga
                                 //de WinMain() y finalice el programa

           return 0; //Sale de la función, no es necesario mandar a procesar
                     //el mensaje con DefWindowProc()
    }
    return DefWindowProc (hwnd, mensaje, wParam, lParam);
}

El primer parámetro es HWND hwnd, que es un identificador de la ventana a la cual pertenece el mensaje. Ya que un mismo procedimiento de ventana puede recibir los mensajes de varias ventanas a la vez (si así se programa creando varias ventanas a partir de la misma clase de ventana WNDCLASSEX), este parámetro nos indica la ventana a la cuál pertenece el mensaje.

El segundo parámetro del procedimiento de ventana, UINT mensaje, especifica el mensaje que se está procesando. Los dos últimos parámetros, wParam y lParam, contienen información adicional que complementan al mensaje.

Errores comúnes en C++. Como ha visto, es común utilizar la sentencia switch() para procesar a los mensajes en lugar de if. En caso de utilizar switch(), recuerde que si declarara una variable dentro de un bloque case deberá incluir llaves { } adicionales al respectivo bloque case X: o de otra manera obtendrá un error de compilación. Esto es algo que usted ya debería de saber, pero vale la pena volverlo a mencionar. Además no olvide hacer uso de la instrucción break al finalizar cada bloque.

5.5 De la teoría a la práctica

Sabiendo lo anterior, es posible programar una ventana que responda a las acciones del usuario. El siguiente código interpretará los mensajes WM_CREATE, WM_LBUTTONUP y WM_DESTROY.

  1. Abrir el entorno de desarrollo (en mi caso Dev-C++)
  2. Crear un nuevo proyecto Windows (ver apéndice B.1) que llamaremos Ejercicio4.
  3. Agregar un Archivo C++ a nuestro proyecto (ver apéndice B.2) al que llamaremos Programa4.
  4. Escribir el siguiente código en el archivo Programa4.cpp. Puede hacer uso del Copy&Paste para agilizar, más adelante analizaremos las partes del código correspondientes a la creación de controles.
Programa4.cppCopiar cógido
#include <windows.h>
#include <sstream>

LRESULT CALLBACK ProcedimientoVentana(HWND hwnd, UINT mensaje,
                                      WPARAM wParam, LPARAM lParam)
{
   switch (mensaje)
   {
      case WM_CREATE:
      {
         MessageBox(hwnd, "Se creó la ventana", "Ejemplo",
                    MB_ICONINFORMATION | MB_OK);
         break;
      }
      case WM_LBUTTONUP:
      {
         int xPos = LOWORD(lParam);
         int yPos = HIWORD(lParam);

         std::ostringstream os;
         os << "Hizo clic con el botón izquierdo";

         if (wParam == MK_CONTROL)
            os << " mientras oprimía la tecla CONTROL";
         else if (wParam == MK_SHIFT)
            os << " mientras oprimía la tecla SHIFT";

         os << "\n\nCoordenadas: " << xPos << ", " << yPos;

         MessageBox(hwnd, os.str().c_str(), "Clic!", MB_ICONINFORMATION | MB_OK);
         break;
      }
      case WM_DESTROY:
      {
         MessageBox(hwnd, "¡Adiós mundo cruel!", "Cerrando...",
                    MB_ICONERROR | MB_OK);

         PostQuitMessage (0);  //Manda WM_QUIT a la cola de mensajes que hará
                               //que el programa salga del bucle de mensajes
         return 0;
      }
   }
   return DefWindowProc (hwnd, mensaje, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstancia, HINSTANCE hInstanciaPrev,
                   LPSTR lpLineaCmd, int nEstadoVentana)
{
    const char szNombreAplicacion[] = "Programa4";
    HWND hwnd;
    MSG mensaje;
    WNDCLASSEX ventana;

    //Configuramos la ventana
    ventana.cbSize = sizeof(ventana);
    ventana.style = CS_HREDRAW | CS_VREDRAW;
    ventana.lpfnWndProc = ProcedimientoVentana;
    ventana.hInstance =  hInstancia;
    ventana.lpszClassName = szNombreAplicacion;
    ventana.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    ventana.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
    ventana.hCursor = LoadCursor (NULL, IDC_ARROW);
    ventana.lpszMenuName = NULL;
    ventana.cbClsExtra = 0;
    ventana.hbrBackground = (HBRUSH)(COLOR_HIGHLIGHT+1);
    ventana.cbWndExtra = 0;


   // Registrar la clase de ventana, si falla, salir del programa
   if(!RegisterClassEx(&ventana)) return (FALSE);

   hwnd = CreateWindowEx(
           0,
           szNombreAplicacion,     //Nombre de clase de la ventana a la que pertenece
           szNombreAplicacion,     //Título de la ventana
           WS_OVERLAPPEDWINDOW,
           CW_USEDEFAULT,
           CW_USEDEFAULT,
           200,                    //Ancho
           200,                    //Alto
           HWND_DESKTOP,
           NULL,
           hInstancia,
           NULL);

   ShowWindow(hwnd, SW_SHOWMAXIMIZED);

   // Bucle de mensajes:
   while(GetMessage(&mensaje, 0, 0, 0) > 0)
   {
      DispatchMessage(&mensaje);
   }

   return mensaje.wParam;
}
  1. Generar el proyecto (ver apéndice B.3) y esperar un momento mientras se genera el ejecutable.

Al mandar llamar a la función CreateWindowEx() para crear a la ventana del programa, Windows manda el mensaje WM_CREATE. El procedimiento de ventana recibe este mensaje después de que la ventana es creada y antes de que la ventana sea visible al usuario. Este mensaje es ideal para crear controles y ventanas dependientes de la principal (ventana padre) como se verá en el siguiente capítulo. En este caso, al procesar al mensaje WM_CREATE, se muestra un texto diciendo "Se creó la ventana".

fig. 5.5.1 Mensaje mostrado al recibir el mensaje WM_CREATE

Cada vez que el usuario de un clic izquierdo sobre la ventana, Windows mandará el mensaje WM_LBUTTONUP, en lParam tendrá el valor de las coordenadas relativas a la ventana de la aplicación y wParam indicará la opresión de ciertas teclas virtuales tales como CONTROL o SHIFT.

fig. 5.5.2 Mensaje mostrado al recibir el mensaje WM_LBUTTONUP
fig. 5.5.3 Mensaje mostrado al recibir el mensaje WM_LBUTTONUP manteniendo la tecla CONTROL oprimida
fig. 5.5.4 Mensaje mostrado al recibir el mensaje WM_LBUTTONUP manteniendo la tecla SHIFT oprimida

Al cerrar la aplicación, ya sea presionando ALT + F4 o sobre el botón cerrar (X) de la ventana, la aplicación recibirá el mensaje WM_DESTROY, el cual será procesado en el procedimiento de ventana mostrando al usuario la alerta "¡Adiós mundo cruel!" y se llamará a PostQuitMessage(0) que sirve para salir del bucle de mensajes y en este caso finaliza la ejecución de la aplicación.

fig. 5.5.5 Mensaje mostrado con el mensaje WM_DESTROY

El resto de los mensajes que pudiera recibir la aplicación por diversas acciones del usuario sobre la ventana de la aplicación, tales como minimizar la ventana o cambiar su tamaño, serán procesados por Windows mediante la función DefWindowProc(). Esta función reenvía el mensaje al procedimiento de ventana por omisión.

5.6 Conclusión

Como ha podido apreciar, una aplicación en Windows no escribe directamente a la pantalla ni utilizar interrupciones de hardware. Estas son completamente distintas a las antiguas aplicaciones de consola (DOS o LINUX). En lugar de ello, la aplicación utiliza funciones disponibles en la API de Windows para recibir e enviar mensajes.

Es por medio de mensajes que Windows difunde información en un entorno multitarea. Para nosotros, un mensaje es una notificación de que ha ocurrido un evento de interés que puede necesitar una respuesta específica. Una aplicación Windows debe estar totalmente orientada al procesamiento de mensajes.



comentarios@rickygzz.com.mx