el Guille, la Web del Visual Basic, C#, .NET y más...

Cambiando el comportamiento predefinido de un control (IV)

Creación de nuevos mensajes

 

Autor: RFOG (MVP de Visual C++)
Publicado: 21/Dic/2010
Actualizado: 21/Dic/2010

Ahora que ya hemos redefinido y ampliado el comportamiento del control, es tiempo de hacer algo para que el contenedor o propietario sepa que han ocurrido eventos como la creación de un nuevo elemento.




 

Introducción:

...

Ahora que ya hemos redefinido y ampliado el comportamiento del control, es tiempo de hacer algo para que el contenedor o propietario sepa que han ocurrido eventos como la creación de un nuevo elemento.

Si nuestro programa utilizara la propia clase CMyVSListbox como elemento activo dentro de nuestro proyecto, el trabajo se habría acabado. Pero resulta que con cosas como el modelo Documento/Vista y ciertas técnicas orientadas a objetos, y pese a que es bueno que cada perro se lama su pijo, digo que cada objeto sepa cómo comportarse a sí mismo, a veces eso no es lo deseable ya que entonces el código real de nuestra aplicación está desperdigado entre muchos elementos diferentes. Además, no es buena idea cargar el código de nuestro proyecto en las clases de interfaz como es la que estamos viendo.

Por lo tanto tenemos que encontrar una nueva forma de comunicar los cambios de la interfaz. En .NET lo habitual es crear un evento y que quienquiera que se suscriba a él. MFC no trabaja de forma tan moderna, pero tiene algo completamente equivalente y que es de donde se sacó la idea del evento: pasar un mensaje a quien quiera que le interese.

Podemos pasar tanto mensajes existentes de Windows o de MFC o como crear los nuestros propios. Pese a que hay varias formas de tener mensajes personalizados, nosotros usaremos la más segura de todas, con la que evitaremos que alguien que vaya a usar nuestro control modificado pueda crear sus propios mensajes personalizados y que estos coincidan con el nuestro.

Aunque a simple vista puede parecer absurdo que se pueda producir una cosa así, es más común de lo que parece, y si lo hacemos nos podemos encontrar con problemas casi completamente insolubles.

Un mensaje realmente es un entero sin signo de 32 bits, o más bien el código que representa al mensaje lo es. MFC (y Win32) deja un rango de mensajes para que los programadores los usen. En general hay dos rangos documentados, que empiezan en WM_USER y WM_APP, y otros de uso completamente libre.

Si suponemos que un control empieza a crear sus mensajes a partir de WM_USER, y que luego en otro programa se usa dicho control y a su vez se necesitan otros mensajes nuevos que también se toman de WM_USER, es muy probable que ambos valores sean iguales, ya que lo habitual es usar WM_USER+1, WM_USER+2, etc. Y tampoco vale pensar que comenzando por el valor más alto, o a partir de uno más o menos aleatorio dentro del rango.

Por tanto, a la hora de crear mensajes que puedan ser utilizados en otras aplicaciones, lo mejor es hacerlo según la siguiente técnica:

  1.  Definir un valor constate y estático del tipo UINT, mejor si pertenece a la clase asociada al mensaje, aunque no es obligatorio.
  2.  En la inicialización de la variable, llamar a RegisterWindowMessage() pasando una cadena única. Lo recomendado es pasar un UUID generado aleatoriamente.
  3.  Definir qué van a contener los parámetros WPARAM y LPARAM. Esta parte es completamente libre, con la condición de que si el mensaje se va a enviar a través de la barrera del proceso, no pueden ser punteros a un bloque de memoria.
  4.  Usar dicha constante para enviar el mensaje a quien queramos mediante SendMessage().
  5.  Para capturar el mensaje, la clase que lo vaya a hacer tiene que utilizar la macro ON_REGISTERED_MESSAGE dentro de su mapa de mensajes.

¿Cómo se aplica esto a nuestro ejemplo? Pues vamos a añadir una serie de mensajes nuevos que servirán para notificar al cuadro de diálogo los eventos conforme vayan ocurriendo.

Lo primero es declarar las variables constantes y estáticas en la clase CMyVSListBox:

class CMyVSListbox : public CVSListBox
{
       DECLARE_DYNAMIC(CMyVSListbox)

public:
       CMyVSListbox();
       virtual ~CMyVSListbox();

       static UINT WM_CUSTOM_ITEM_CHANGED;
       static UINT WM_CUSTOM_ON_BEFORE_REMOVE_ITEM;
       static UINT WM_CUSTOM_ON_AFTER_ADD_ITEM;
       static UINT WM_CUSTOM_ON_AFTER_RENAME_ITEM;

       virtual void OnSelectionChanged() {}

       // "Standard" action overrides
       virtual BOOL OnBeforeRemoveItem(int /*iItem*/);
       virtual void OnAfterAddItem(int /*iItem*/);
       virtual void OnAfterRenameItem(int /*iItem*/);
       virtual void OnAfterMoveItemUp(int /*iItem*/){}
       virtual void OnAfterMoveItemDown(int /*iItem*/){};

protected
:
       DECLARE_MESSAGE_MAP()
};

 

 

Las hemos llamado WM_CUSTOM_<nombre_común> por dos motivos. El primero es que inmediatamente se ve que son mensajes “WM” y que son personalizados “CUSTOM”. El nombre que viene después, y siguiendo las reglas de nomenclatura de MFC, tienen el nombre del evento.

Observe el lector que hemos eliminado el cuerpo de alguno de los métodos On<texto>().

Ahora, en algún lugar del fichero fuente de la clase, colocamos los inicializadores:

UINT CMyVSListbox::WM_CUSTOM_ITEM_CHANGED=RegisterWindowMessage(_T("WM_CUSTOM_ITEM_CHANGED"));
UINT CMyVSListbox::WM_CUSTOM_ON_BEFORE_REMOVE_ITEM=RegisterWindowMessage(_T("WM_CUSTOM_ON_BEFORE_REMOVE_ITEM"));
UINT CMyVSListbox::WM_CUSTOM_ON_AFTER_ADD_ITEM=RegisterWindowMessage(_T("WM_CUSTOM_ON_AFTER_ADD_ITEM"));
UINT CMyVSListbox::WM_CUSTOM_ON_AFTER_RENAME_ITEM=RegisterWindowMessage(_T("WM_CUSTOM_ON_AFTER_RENAME_ITEM"));

 

Aquí no hay truco. Para inicializar una variable estática y constante, tenemos que asignarla fuera de la clase y del programa, y en nuestro caso lo hacemos llamando a RegisterWindowMessage(). Si el autor recuerda bien, estas variables son inicializadas en la carga del programa, cuando se inicializan todas las variables estáticas y globales.

YYa solo nos queda implementar el cuerpo de los métodos:

BOOL CMyVSListbox::OnBeforeRemoveItem(int iItem)
{
       return GetOwner()->SendMessage(WM_CUSTOM_ON_BEFORE_REMOVE_ITEM,(WPARAM)iItem,(LPARAM)NULL)==TRUE;
}

void CMyVSListbox::OnAfterAddItem(int iItem)
{
       GetOwner()->SendMessage(WM_CUSTOM_ON_AFTER_ADD_ITEM,(WPARAM)iItem,(LPARAM)NULL);
}

void CMyVSListbox::OnAfterRenameItem(int iItem)
{
       GetOwner()->SendMessage(WM_CUSTOM_ON_AFTER_RENAME_ITEM,(WPARAM)iItem,(LPARAM)NULL);
}

 

Fijaros en que lo que hacemos es obtener un handle al propietario del control (en nuestro caso la clase que representa al cuadro de diálogo) y le enviamos el mensaje.

 

Si prestamos atención al método OnBeforeRemoveItem(), el resultado del envío del mensaje se compara con TRUE, y es lo que se devuelve en dicha función, indicando si queremos que el elemento sea borrado o no. En este caso hemos transferido la decisión a quien quiera que esté controlando esta clase.

Debemos hacer notar que una llamada a SendMessage() espera hasta que el receptor, si hay alguno, capture y responda adecuadamente al mensaje, por lo que los mensajes son como los eventos de .NET: cuanto menos código más rápida irá nuestra aplicación. Si queremos un retorno inmediato, podríamos usar PostMessage(), pero entonces no podríamos obtener el valor de retorno sin complicar –bastante- las cosas.

La idea subyacente en SendMessage() es bastante inteligente, porque estamos usando una especie de puntero a función genérica sin necesidad de toda la sintaxis de los mismos, y encima podemos hacer una llamada a múltiples funciones etc.: lo mismo que hacen los delegados en .NET pero en nativo (y de donde se copió).

Ahora tenemos que irnos al cuadro de diálogo, y le añadimos un control de edición normal y corriente al lado derecho, creando a su vez una variable que lo represente con el nombre de “c_edit”.

UUna vez hecho esto, añadimos la captura de mensajes en el cuadro de diálogo:

class Ccambiar_controlDlg : public CDialogEx
{
// Construction
public:
       Ccambiar_controlDlg(CWnd* pParent = NULL);     // standard constructor

// Dialog Data
       enum { IDD = IDD_CAMBIAR_CONTROL_DIALOG };

      
protected:
       virtual void DoDataExchange(CDataExchange* pDX);      // DDX/DDV support

// Implementation
protected:
       HICON m_hIcon;

       // Generated message map functions
       virtual BOOL OnInitDialog();
       afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
       afx_msg void OnPaint();
       afx_msg HCURSOR OnQueryDragIcon();
       DECLARE_MESSAGE_MAP()

       virtual afx_msg LRESULT OnBeforeRemoveItem(WPARAM,LPARAM);
       virtual afx_msg LRESULT OnAfterRenameItem(WPARAM,LPARAM);
       virtual afx_msg LRESULT OnAfterAddItem(WPARAM,LPARAM);

public:
       CMyVSListbox c_lb;
       CEdit c_edit;
};

 

¿Por qué virtuales? Pues pese a la ínfima caída de rendimiento (que no se va a notar a no ser que nuestra clase tenga cientos o miles de métodos virtuales o que estos se llamen en bucle), si queremos heredar un nuevo diálogo del nuestro sólo tendremos que implementar dichos métodos y obviar todo el tema de los mensajes, que ya hace la clase padre.

FFinalmente creamos el mapa de mensajes:

BEGIN_MESSAGE_MAP(Ccambiar_controlDlg, CDialogEx)
        ON_WM_SYSCOMMAND()
        ON_WM_PAINT()
        ON_WM_QUERYDRAGICON()
        ON_REGISTERED_MESSAGE(CMyVSListbox::WM_CUSTOM_ITEM_CHANGED,&OnListBoxItemChanged)
        ON_REGISTERED_MESSAGE(CMyVSListbox::WM_CUSTOM_ON_BEFORE_REMOVE_ITEM,&OnBeforeRemoveItem)
        ON_REGISTERED_MESSAGE(CMyVSListbox::WM_CUSTOM_ON_AFTER_RENAME_ITEM,&OnAfterRenameItem)
        ON_REGISTERED_MESSAGE(CMyVSListbox::WM_CUSTOM_ON_AFTER_ADD_ITEM,&OnAfterAddItem)
END_MESSAGE_MAP()

 

Y los propios métodos:

LRESULT Ccambiar_controlDlg::OnListBoxItemChanged(WPARAM,LPARAM)
{
       c_edit.SetWindowText(_T("Item chaned"));
       return TRUE;
}

LRESULT Ccambiar_controlDlg::OnBeforeRemoveItem(WPARAM,LPARAM)
{
       c_edit.SetWindowText(_T("Item removed"));
       return true;
}

LRESULT Ccambiar_controlDlg::OnAfterRenameItem(WPARAM,LPARAM)
{
       c_edit.SetWindowText(_T("Item renamed"));
       return TRUE;
}

LRESULT Ccambiar_controlDlg::OnAfterAddItem(WPARAM,LPARAM)
{
       c_edit.SetWindowText(_T("Item added"));
      
return TRUE;
}

 

Existe un pequeño problema con el método OnListBoxItemChanged().. ¿Cuál es el nuevo elemento? ¿Y el antiguo? Imaginaros una lista maestro/detalle, en la que cada vez que el elemento seleccionado cambie, tenemos que realizar algunas tareas. Lo vamos a dejar como ejercicio para el lector, que tiene todas las pistas necesarias para realizar la tarea leyendo todas las entradas de la serie sobre MFC.

 

Código fuente:

El lector tiene el código fuente de esta entrada disponible en el fichero:
MFC_10_cambiar_control_03.zip


(MD5 checksum: F9D656FDC62596270D30D581FF3DD040)

 

 


Ir al índice de los artículos de RFOG en el sitio del Guille




La fecha/hora en el servidor es: 23/11/2024 11:50:35

La fecha actual GMT (UTC) es: 

©Guillermo 'guille' Som, 1996-2024