Pre-Introducción
Tras la escritura de este documento,
Rodrigo Corral me informó de que las declaraciones adelantadas sí que funcionan
con Forms
en C++/CLI, pero que el tema es algo complicadillo y hay que hacerlo con truco, por lo que ha escrito una
entrada sobre ello en su blog.
Introducción
Este documento surge como consecuencia de una
duda planteada en el grupo de noticias del Visual C++ de
Microsoft, y pretende ser una guía rápida para solventar el tema de las
dependencias circulares que se presentan en el mundo C++/CLI al no estar
permitidas las variables globales.
Otros lenguajes CLI lo solucionan permitiendo
dicho tipo de dependencias, pero el C++/CLI, dadas sus características únicas,
no debe permitirlas, y de hecho no lo hace.
El
problema
Imaginemos una situación como la siguiente:
tenemos una ventana padre y otra hija, no en el sentido de la herencia del C++,
sino en el de que la ventana padre posee
a la hija, y la controla.
La ventana padre ha de conocer la definición de
la hija, por lo que se debe incluir el fichero cabecera correspondiente.
Pero podemos necesitar que, al igual que la
ventana padre controle a la hija, ésta también deba manejar a su propietario.
Para ello, también debemos incluir la
definición de la clase padre en la hija, con lo que generamos la dependencia
circular.
El motivo de esta necesidad circular se puede
observar claramente si estuviéramos construyendo un editor de textos. La
ventana principal del editor ha de abrir otra ventana, que quedará no modal y
en la que el usuario tecleará el texto a buscar. Cuando seleccione “buscar” en
la ventana hija, ésta ha de comunicarle a la principal que realice las tareas
pertinentes, por lo que debe conocerla. Y es aquí donde se produce la
dependencia circular.
La
solución clásica
La solución clásica es muy sencilla, a la hija se
le pasa el handle
de ventana padre (o este handle
es global), y cuando la primera necesita comunicarle algo, le envía un mensaje
a través de PostMessage
o SendMessage.
Dicho mensaje deberá estar en el rango de WM_USER, y la
ventana padre deberá poder capturarlo e interpretarlo.
Puesto con código fuente, la solución quedaría
más o menos como sigue:
//En “stdafx.h”
o en otro lugar común a ambas ventanas. |
#define WM_USER_SEARCH WM_USER+1 |
|
//En la ventana padre,
dentro del bucle de mensajes: |
… |
case WM_USER_SEARCH: |
//Código de búsqueda |
break; |
|
//En la ventana hija, en el
mensaje WM_COMMAND, procesando |
//el clic de “buscar” |
PostMessage( hwnd_ventanaPadre,
WM_USER_SEARCH, |
(WPARAM)m_tipoBusqueda, (LPARAM)m_pCadenaBuscar); |
La solución MFC es muy similar a esta. Así,
aparte de hacer ambas ventanas completamente independientes una de otra,
evitamos que la ventana hija tenga que conocer toda la ventana padre, haciendo
que toda la comunicación entre ellas se lleve a cabo mediante una variable del
tipo Handle de Ventana.
La
solución CLI
En el mundo .NET el sistema de paso de mensajes
desparece de la vista del programador y queda subsumido dentro del marco de
aplicación, liberando así de cierta complejidad, pero a su vez generando otra
nueva.
La solución CLI pasa por el uso de delegados, o
mejor todavía, de eventos, aunque aquí sólo vamos a explicar la primera de las
dos formas, siendo la segunda trivial de implementar conociendo la anterior.
Como ejemplo vamos a tener dos ventanas, al hacer
clic en la principal se nos abrirá la hija, y al hacer clic sobre esta,
aparecerá en la padre un mensaje.
El primer paso consiste en definir el delegado de
forma global y que sea visible para ambas ventanas. En nuestro caso será un
método al que se le pasará una cadena, que será mostrada en un componente de
etiqueta. La firma del método será, pues:
delegate void MuestraMensaje(String ^msg) |
Esta línea se colocará en el fichero cabecera de
la ventana hija; como este fichero estará incluido en el fichero cabecera de la
ventana padre, ambas ventanas conocerán el delegado.
El siguiente paso será añadir una variable del
tipo anterior en la ventana hija, que contendrá una referencia al método de la
ventana padre, y que se ejecutará cuando así lo desee la ventana hija.
Para el programador de C++ tradicional, el
concepto equivalente es sencillo: la ventana hija guarda un puntero a un método
de la ventana padre, método que será ejecutado cuando dicha variable sea
llamada.
Veamos el código:
public: MuestraMensaje ^m_muestra; |
|
private:
void ChildForm_Click(Object^ sender, EventArgs^ e) |
{ |
m_muestra("hola"); |
} |
La primera línea define la variable m_muestra, que es
una variable miembro referencia del delegado MuestraMensaje. El equivalente
C++ sería el puntero al método. En justa medida dicha variable miembro debería
ser privada o protegida, y ser asignada mediante un método público o una
propiedad, pero la hemos puesto pública aquí para no liar mucho el código.
Ya sólo nos queda ver la parte de la ventana
padre. Lo primero que tenemos que hacer es incluir el fichero cabecera de la
ventana hija para que la ventana padre la conozca, a ella y al delegado.
#include
“ChildForm.h” |
|
//Este es el método que la
hija va a llamar |
public: void Mensaje(String ^msg) |
{ |
label1->Text=msg; |
} |
|
//Aquí construimos la
ventana hija y asignamos el delegado. |
private: void Form1_Click(Object^ sender, System::EventArgs^ e) |
{ |
ChildForm
^f=gcnew ChildForm(); |
f->m_muestra=gcnew MuestraMensaje(this,&Form1::Mensaje); |
f->Show(); |
} |
El primer método implementa la función que va a
ser llamada por la ventana hija sobre la padre cuando
así lo queramos. En nuestro caso se trata simplemente de poner un texto en un label, pero
podría haber sido cualquier cosa.
El siguiente método es la respuesta a hacer clic
sobre la ventana padre; creamos una ventana hija, le asignamos el delegado, y
la mostramos. La línea central del método lo que hace es crear un enlace entre el método Mensaje de la clase padre y almacenarlo
en la variable m_muestra
de la hija.
Y con eso hemos terminado. El asunto funciona de
la siguiente manera. Se crea la ventana padre, y cuando el usuario hace clic
sobre ella, dispara el evento Form1_Click, que crea una nueva instancia de la clase ChildForm, le
asigna un delegado que se corresponde con el método Mensaje, y muestra la ventana.
Tantas veces como hagamos clic sobre la ventana
padre, tantas ventanas hijas independientes se crearán, todas ellas almacenando
el delegado correspondiente. Los puristas de C++, como yo, afirmarán que
estamos dejando sueltas muchas referencias. La respuesta es que sí, pero como
se trata de objetos CLI, éstos se autodestruirán durante la recolección de
basura, sin tener que preocuparnos nosotros de ello.
Ahora, cuando un usuario haga clic en cualquier
ventana hija, disparará su evento ChildForm_Click correspondiente, que ejecutará el delegado m_muestra, quien
a su vez lanzará el método Muestra de
la ventana padre, habiendo conseguido nuestro propósito a la manera .NET.
Final
El ejemplo se puede bajar de http://rfog.cmact.com/test.zip. El
que quiera adaptar esto a otro lenguaje CLI, que me lo diga y lo haga (y me
diga dónde lo publica o si lo publico yo).