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:
- Definir un valor constate y estático del tipo UINT, mejor si
pertenece a la clase asociada al mensaje, aunque no es obligatorio.
- En la inicialización de la variable, llamar a
RegisterWindowMessage() pasando una cadena única. Lo recomendado es
pasar un UUID generado aleatoriamente.
- 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.
- Usar dicha constante para enviar el mensaje a quien queramos
mediante SendMessage().
- 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.
Ir al índice de los artículos de RFOG en el
sitio del Guille