por Ricardo González Garza
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 cotnrol. 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 identificador de ventana del control en cuestion 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 wndOld;
   wndOld = (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 mensaje,
                                    WPARAM wParam, LPARAM lParam)
{
   switch (mensaje)
   {
      //Manipulación de los eventos de hCONTROL
   }
   return CallWindowProc (wndOld, hwnd, mensaje, 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 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 (un miembro estático). 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. Esta última función devolverá el antiguo procedimiento, el cual se deberá almacenar para su uso en el paso 5.
  3. Crear un miembro estático despachador encargado 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
{
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);
   }

private:

   WNDPROC ViejoProc;
};

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 al usuario introducir números. 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 y hacer esto posible.

  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). 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,       //Extended window style
                                   "EDIT",       //Classname
                                   lpText,            //Text
                                   WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,           //Window style
                                   x, y, w, h,
                                   hPADRE,
                                   NULL,              //Child window identifier
                                   NULL,              //Value ignored on Win2000/XP/NT
                                   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í:

Utilice el programa generado. Intente introducir información numérica y no numérica para ver el comportamiento del control. Observe 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.

fig. 7.3.1 Ventana correspondiente al Ejercicio 6

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 se mandan antes de que sea ejecutado el código delante de CreateWindowEx() y no pasan por el bucle de mensajes; esto hace imposible que sea posible guardar el puntero this en los datos de usuario de la ventana usando el método anterior.

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. Bien, es posible modificar el procedimiento de ventana, 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.

¿Qué sucede si se desea procesar alguno de estos mensajes que se ignoran? Normalmente interesa el mensaje WM_CREATE para iniciar variables o incluso añadir ventanas hijas como controles. Existe un método 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 wndclass;

   wndclass.cbSize = ...
   ...
   //2. Asignar el procedimiento de ventana STATIC
   wndclass.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)
{
   WINDOW* 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);
   }
   else
   {
      //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);
   }

   return DefWindowProc (hwnd, iMensaje, wParam, lParam);
}


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

      ...

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

    return DefWindowProc (hwnd, mensaje, wParam, lParam);
}

De esta manera, el procedimiento de ventana Procedimiento recibirá todos los mensajes excepto el WM_GETMINMAXINFO anterior a WM_NCCREATE.

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