Inicio Reflexiones Guitarra Engineering Programación Win32

Subclasificación
Capítulo 7, 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++
- ¿Qué es... ? struct, typedef
Haber leído el Capítulo 2
- Saber generar y correr ejecutables en base a código C++

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

7.1 Subclasificación

Existen muchos controles estándares de Windows disponibles para que el programador pueda reutilizarlos tales como etiquetas, cajas de texto o botones. Cada uno de ellos tiene un procedimiento de ventana distinto que procesa los mensajes y efectua los comportamientos adecuados del control. El procedimiento de ventana para los controles está en algún sitio dentro de Windows. Así, por ejemplo, los mensajes que recibe una caja de texto son procesados por este procedimiento de ventana predeterminado del que hablamos y el programador no debe preocuparse por codificar las acciones cuando el usuario selecciona el texto, lo modifica, lo inserta entre dos letras o hace un Copy&Paste. ¿Pero qué sucede si deseamos todas las características de una caja de texto pero que solo acepte números ignorando cualquier otro caracter?

Es posible obtener la dirección de este procedimiento de ventana mediante una llamada GetWindowLongPtr() usando al manipulador de ventana del control y a GWLP_WNDPROC como parámetros. Se puede definir un nuevo procedimiento de ventana para cada uno de los controles vistos anteriormente llamando a SetWindowLongPtr().

La técnica de cambiar la dirección del procedimiento de ventana se llama subclasificación de ventana y es muy necesario. Permite monitorear y procesar los mensajes que recibe la ventana o control y pasar los restantes al procedimiento de ventana antiguo.

SubclasificaciónCopiar cógido
//1. Obtener el procedimiento actual de la ventana
   WNDPROC ViejoProc;
   ViejoProc = (WNDPROC) GetWindowLongPtr(hCONTROL, GWLP_WNDPROC);

//2. Cambiar el procedimiento por NuevoProcedimiento()
   SetWindowLongPtr(hCONTROL, GWLP_WNDPROC, (LONG_PTR) NuevoProcedimiento);

//3. Manejar los mensajes de interés
LRESULT CALLBACK NuevoProcedimiento(HWND hwnd, UINT iMensaje,
                                    WPARAM wParam, LPARAM lParam)
{
   switch (iMensaje)
   {
      //Manipulación de los eventos de hCONTROL
   }
   return CallWindowProc (ViejoProc, hwnd, iMensaje, wParam, lParam);
}

En el capítulo anterior, se utilizó la función DefWindowProc() en el procedimiento de ventana para procesar los mensajes con acciones por defecto que no desean modificarse. Cuando se subclasifica una ventana, es necesario utilizar CallWindowProc() que es idéntica a la anterior, pero agrega un parámetro para señalar la antigua función de procedimiento de la ventana subclasificada.

7.2 Subclasificación en C++

Si desea utilizar las bondades de la programación orientada a objetos en C++ para crear controles a la medida, la técnica de subclasificación es un poco más laboriosa. Una ventaja de utilizar objetos que tienen constructores y destructores, es la posibilidad de olvidarse de liberar recursos innecesarios si se programa una librería lo suficientemente robusta. Otra ventaja es que al utilizar objetos, la programación se torna mucho más intuitiva. Esto se refleja en un ejecutable más sólido.

Como probablemente sabe, el puntero this de una clase en C++ se pasa como un parámetro invisible. Las funciones API de Windows no están preparadas para manejar objetos ni punteros this. Por lo tanto, cuando se quiere utilizar la función SetWindowLongPtr() con GWLP_WNDPROC, se deberá indicar una función destino que no tenga dicho puntero.

Quizás ya tenga en mente la manera de lidiar con esto, existen diversos caminos que se pueden tomar. Una forma sería utilizando la función miembro static dentro de una clase C++ ya que no utiliza el puntero this. Considere el siguiente método:

  1. Almacenar el puntero this. Se llama a SetWindowLongPtr() con el parámetro GWLP_USERDATA para almacenar al puntero this en el dato de usuario. El dato de usuario es un espacio para alojar información asociada a la ventana y existe para que el programador pueda almacenar información a su conveniencia.
  2. Subclasificar, es decir, cambiar el procedimiento de ventana por defecto en el control por una función despachadora sin puntero this (el miembro estático del paso 3). Esto se hace como ya se ha visto: Utilizando la función SetWindowLongPtr() con GWLP_WNDPROC para indicar un procedimiento de ventana destino, en este caso, será el despachador. Recuerde que la función SetWindowLongPtr() devolverá el puntero al antiguo procedimiento, el cual se deberá almacenar para su uso en el paso 5.
  3. Crear un miembro estático despachador, está función no tiene puntero this que hace posible subclasificar hacia ésta en el paso 2. Esta función se encargará de obtener el puntero this almacenado en el paso 1 y llamar al procedimiento de mensajes adecuado.
  4. Procesar los mensajes según las necesidades.
  5. Procesar los mensajes en su procedimiento por defecto si se desea el comportamiento predeterminado. Se utiliza CallWindowProc() con el antiguo procedimiento devuelto en el paso 2.
Código utilizado para subclasificar en OOPCopiar cógido
class CONTROL_SUBCLASIFICADO
{
private:
   //Aquí se guardará el procedimiento de ventana por defecto de Windows
   WNDPROC ViejoProc;

public:
   CONTROL_SUBCLASIFICADO() { ... }  //Constructor

   ~CONTROL_SUBCLASIFICADO() { ... }; //Destructor

   void Create(HWND hPADRE, LPCTSTR lpTexto, UINT x, UINT y, UINT w, UINT h)
   {
      ...

      hCONTROL = CreateWindowEx(...);

      ...

/* 1. Guardar el puntero this en los datos de usuario del control (GWLP_USERDATA)
   ============================================================================== */
      SetWindowLongPtr(hCONTROL, GWLP_USERDATA, (LONG_PTR) this);

/* 2. Subclasificar a una función estática despachadora (sin puntero this)
   ======================================================================= */
      ViejoProc = (WNDPROC) SetWindowLongPtr(hCONTROL, GWLP_WNDPROC,
                                             (LONG_PTR) Despachador);
   }

/* 3. Función estática que obtiene el puntero this previamente almacenado en los
      datos de usuario del control y llama al Procedimiento de Mensajes adecuado
   ============================================================================= */
   static LRESULT CALLBACK Despachador (HWND hwnd, UINT iMensaje,
                                        WPARAM wParam, LPARAM lParam)
   {
      CONTROL_SUBCLASIFICADO* pControl;
      pControl = (CONTROL_SUBCLASIFICADO *) GetWindowLongPtr(hwnd, GWLP_USERDATA);

      //Despachar a procedimiento correspondiente al control
      return pControl->Procedimiento (hwnd, iMensaje, wParam, lParam);
   }

/* 4. Procedimiento de Ventana
   =========================== */
   LRESULT CALLBACK Procedimiento(HWND hwnd, UINT iMensaje,
                                  WPARAM wParam, LPARAM lParam)
   {
      switch (iMensaje)
      {
         ...
      }

/* 5. Procesar mensajes en procedimiento de ventana por defecto
   ============================================================ */
      return CallWindowProc (ViejoProc, hwnd, iMensaje, wParam, lParam);
   }
};

7.3 De la teoría a la práctica

La finalidad de este ejercicio es modificar el comportamiento del control de caja de texto (EDIT) para que solo permita introducir números al usuario. Cualquier otro caracter, deberá ser ignorado por el control. Es una modificación básica que tiene fines didácticos. Este nuevo control no permitirá escribir números negativos (signo -) ni indicar comas o puntos de separación; se deja el reto al lector para añadir el código necesario para hacer posible esto último.

  1. Abrir el entorno de desarrollo (en mi caso Dev-C++)
  2. Crear un nuevo proyecto Windows (ver apéndice B.1) que llamaremos Ejercicio6.
  3. Agregar un Archivo de encabezado a nuestro proyecto (ver apéndice B.2) al que llamaremos ventana.h (todo en minúsculas). Use el mismo código que en el capítulo anterior.
  4. Agregar otro Archivo de encabezado a nuestro proyecto al que llamaremos edit_num.h (todo en minúsculas). Este contendrá el código de la clase caja de texto numérica. Copiar el siguiente código.
edit_num.hCopiar cógido
#include <windows.h>

class EDIT_NUMERICO
{
public:
   EDIT_NUMERICO() { hPADRE = hCONTROL = 0; }  //Constructor

   ~EDIT_NUMERICO() {}; //Destructor

   void Create(HWND hWindow, LPCTSTR lpText, UINT x, UINT y, UINT w, UINT h)
   {
      hPADRE = hWindow;
      if (!hCONTROL)
         hCONTROL = CreateWindowEx(WS_EX_CLIENTEDGE,
                                   "EDIT",
                                   lpText,
                                   WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,
                                   x, y, w, h,
                                   hPADRE,
                                   NULL,
                                   NULL,
                                   NULL);

      if (hCONTROL == NULL)
         MessageBox(hPADRE, "Ocurrió un error al crear el control.",
                    "¡Error!", MB_OK | MB_ICONERROR);
      else
      {  //Cambiar fuente del control
         SendMessage(hCONTROL, WM_SETFONT, (WPARAM) GetStockObject(DEFAULT_GUI_FONT),
                     MAKELPARAM(FALSE, 0));

/* 1. Guardar el puntero this en los datos de usuario del control (GWLP_USERDATA)
   ============================================================================== */
         SetWindowLongPtr(hCONTROL, GWLP_USERDATA, (LONG_PTR) this);

/* 2. Subclasificar a una función estática despachadora (sin puntero this)
   ======================================================================= */
         ViejoProc = (WNDPROC) SetWindowLongPtr(hCONTROL, GWLP_WNDPROC,
                                                (LONG_PTR) DispatchProc);
      }
   }

/* 3. Función estática que obtiene el puntero this previamente almacenado en los
      datos de usuario del control y llama al Procedimiento de Mensajes adecuado
   ============================================================================= */
   static LRESULT CALLBACK DispatchProc (HWND hwnd, UINT iMensaje,
                                         WPARAM wParam, LPARAM lParam)
   {
      EDIT_NUMERICO* pControl;
      pControl = (EDIT_NUMERICO *) GetWindowLongPtr(hwnd, GWLP_USERDATA);

      //Despachar a procedimiento correspondiente al control
      return pControl->Procedimiento (hwnd, iMensaje, wParam, lParam);
   }

/* 4. Procedimiento de Ventana
   =========================== */
   LRESULT CALLBACK Procedimiento(HWND hwnd, UINT iMensaje,
                                  WPARAM wParam, LPARAM lParam)
   {
      if (iMensaje == WM_PASTE)
         return 0; //prohibimos pegar pues podría contener texto

      if (iMensaje == WM_CHAR)
      {  //Si no es la tecla de retroceso ni número del 0 al 9 ignorar pulsación
         if (((wParam < '0') && (wParam != 0x08)) || (wParam > '9'))
            return 0;
      }

/* 5. Procesar mensajes en procedimiento de ventana por defecto
   ============================================================ */
      return CallWindowProc (ViejoProc, hwnd, iMensaje, wParam, lParam);
   }

private:
      HWND hCONTROL, hPADRE;
   WNDPROC ViejoProc;
};
  1. Agregar un Archivo C++ a nuestro proyecto (ver apéndice B.2) al que llamaremos Programa6.cpp.
  2. Escribir el siguiente código en el archivo Programa6.cpp. Puede hacer uso del Copy&Paste para agilizar.
Programa6.cppCopiar cógido
#include "ventana.h"
#include "edit_num.h"

EDIT_NUMERICO Numerico;


LRESULT CALLBACK ProcedimientoVentana(HWND hPadre, UINT mensaje,
                                      WPARAM wParam, LPARAM lParam)
{
    switch (mensaje)
    {
       case WM_CREATE:
       {
          //Se crea el control
          Numerico.Create(hPadre, "", 10, 10, 180, 20);
          break;
       }
       case WM_DESTROY:
       {
          PostQuitMessage (0);
          return 0;
       }
    }
    return DefWindowProc (hPadre, mensaje, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstancia, HINSTANCE hInstanciaPrev,
                   LPSTR lpLineaCmd, int nEstadoVentana)
{
   CreaVentana("Programa 6", 208, 80, hInstancia, ProcedimientoVentana);

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

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

Observe que el código utilizado en Programa6.cpp consta de unas cuantas líneas. Eso si consideramos que los archivos de encabezado ventana.h y edit_num.h podrán re-utilizarse en proyectos futuros. La ventana generada deberá verse así:

fig. 7.3.1 Ventana correspondiente al Ejercicio 6

Utilice el programa generado. Intente introducir información numérica y no numérica para observar el comportamiento del control. Compruebe que se ha bloqueado la acción de pegar texto en este control modificado con la finalidad de evitar que se introduzcan caracteres no deseados (ej. letras). Analizar la información del portapapeles para saber si se trata de información numérica o de texto queda fuera de las intenciones didácticas de este ejercicio.

7.4 Subclasificación de ventanas en C++

El método para subclasificar en C++ visto en el tema anterior es igualmente válido, aunque con ciertas consideraciones, para ventanas en las que definimos una clase de ventana WNDCLASS propia (no como las clases "BUTTON" o "EDIT" que existen en el estándar de Windows). Tal es el caso de la ventana padre.

A diferencia de los controles (con clases de ventana existentes), en las ventanas con clase de ventana propia se debe considerar que al terminar de llamar a la función CreateWindowEx(), el sistema manda inmediatamente mensajes al procedimiento de ventana. Dichos mensajes no pasan por el bucle de mensajes y se mandan antes de que sea ejecutado el código delante de CreateWindowEx(); esto hace imposible guardar el puntero this en los datos de usuario de la ventana usando el método anterior para procesar estos mensajes generados.

Recuerde que el procedimiento de ventana es indicado en el campo lpfnWndProc de la clase de ventana WNDCLASS. Generalmente se mandan los mensajes WM_GETMINMAXINFO, WM_NCCREATE, WM_NCCALCSIZE y WM_CREATE en ese orden. El mensaje que más interesa procesar es WM_CREATE ya que es ahí donde se deberán iniciar variables o crear controles hijos de la ventana.

Bien, es posible modificar el procedimiento de ventana Despachador, identificar estos mensajes e ignorarlos, ya que no se debe despachar al correspondiente procedimiento de ventana sin tener el puntero this. Quizás el despachador visto anteriormente le funcione sin realizar modificaciones, pero sin el puntero this no será posible acceder a los miembros del objeto en cuestión (objeto->miembro) dentro del procedimiento de ventana cuando procese uno de estos 4 mensajes (ej. WM_CREATE.

¿Qué sucede si se desea procesar alguno de estos mensajes que no pasan por el bucle de mensajes? Existe una técnica para almacenar al puntero this. Cuando se recibe el mensaje WM_NCCREATE indica en el lParam un puntero a la estructura CREATESTRUCT que contiene la información de cada uno de los parámetros de la función CreateWindowEx(). Dicha estructura, tiene un campo llamado lpCreateParams que corresponde al dato del último parámetro de CreateWindowEx() (LPVOID lpParam), en el cual podemos almacenar el puntero this. En código quedaría más o menos así:

Subclasificación de ventana en C++Copiar cógido
class VENTANA
{
public:
   ...
   int Crear( ... );

   //1. Crear un procedimiento static (sin puntero this)
   static LRESULT CALLBACK Despachador( ... );
   LRESULT CALLBACK Procedimiento( ... );

   ...
};

int VENTANA::Crear( ... )
{
   ...
   WNDCLASSEX clsventana;

   clsventana.cbSize = ...
   ...
   //2. Asignar el procedimiento de ventana STATIC
   clsventana.lpfnWndProc = Despachador;
   ...

   //3. Indicar el puntero this en el último parámetro
   hWND = CreateWindowEx( ... , this);

   ...
}

LRESULT CALLBACK VENTANA::Despachador(HWND hwnd, UINT iMensaje,
                                     WPARAM wParam, LPARAM lParam)
{
   VENTANA* pVentana;

   //4. Identificar el mensaje WM_NCCREATE
   if (iMensaje == WM_NCCREATE)
   {
      CREATESTRUCT* c;

      //5. Extraer el puntero this del parámetro lpParam de CreateWindowEx()
      pVentana = (VENTANA*) ((CREATESTRUCT *) lParam)->lpCreateParams;

      //6. Almacenar el puntero this en la información de usuario como se vio en el
      //   tema anterior
      SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR) pVentana);
   }


   //7. Obtener el puntero this de la información de usuario como se vio en el
   //   tema anterior
   pVentana = (VENTANA*) GetWindowLongPtr(hwnd, GWLP_USERDATA);

   //Despachador al procedimiento de ventana correspondiente
   //=======================================================
   if (pVentana == NULL//Mensaje WM_GETMINMAXINFO, procesado por Windows
      return DefWindowProc (hwnd, iMensaje, wParam, lParam);
   else //8. Despachar al procedimiento
      return pVentana->Procedimiento (hwnd, iMensaje, wParam, lParam);
}


LRESULT CALLBACK VENTANA::Procedimiento(HWND hwnd, UINT iMensaje,
                                        WPARAM wParam, LPARAM lParam)
{
   //9. Procesar mensajes
   switch (iMensaje)
   {
      case WM_CREATE:
      {
         //Inicia variables, crea controles, etc
      }
      break;

      ...

      case WM_DESTROY:
         PostQuitMessage (0);
         return 0;
      break;
    }

   //10. Procesar mensajes en procedimiento de ventana por defecto
   return CallWindowProc (ViejoProc, hwnd, iMensaje, wParam, lParam);
}

Observe que no se subclasifica a una función estática despachadora (sin puntero this) ya que esto no es necesario. La función despachadora se indica en el campo lpfnWndProc de la estructura WNDCLASSEX.

De esta manera, el procedimiento de ventana Procedimiento recibirá todos los mensajes. Solamente deberá tener cuidado con el mensaje WM_GETMINMAXINFO (anterior a WM_NCCREATE) ya que se procesa sin el puntero this adecuado. Muy rara vez es necesario procesar este mensaje, pero de requerirse, deberá proceasarse en la función Despachador en vez del miembro Procedimiento.

7.5 Crear su propia librería

Si está pensando en crear su propia librería para realizar proyectos, bien convendría dar un vistazo a proyectos del mismo índole. Se trata de no re-inventar la rueda. Recomiendo el proyecto SmartWin++ cuya librería es completamente gratuita y distribuye el código fuente con comentarios.

7.6 Ejercicios

1. Crear una control EDIT numérico que acepte números positivos, negativos y con decimales. El resto de los caracteres deberán ser ignorados. Deberá ser válido pulsar la tecla de retroceso (backspace). Debe impedirse poner doble separación decimal o escribir un número incorrectamente (ej. "-100.1-2---3.10")

fig. 7.6.1 EDIT numérico correspondiente al ejercicio


comentarios@rickygzz.com.mx