Introducción
Por aclamación popular –y porque me ha picado la curiosidad-, vamos a explicar
cómo obtener todos los controles que haya en un cuadro de diálogo sin conocer ni sus HWND de ventana ni sus ID de recurso.
Al tajo
Así dicho a bote pronto parece toda una hazaña digna de Hércules, pero lo cierto
es que resulta mucho más sencillo de lo que a primera vista parece si se cae en
ello. Para que nadie se haga ilusiones, he de añadir que de esta forma no es
posible controlar una aplicación que no sea la misma que ejecuta este código, o
al menos no sin antes emplear otras técnicas de inyección de código que no voy a
contar pero que están disponibles para aquellos que lean la documentación
pertinente y adecuada. Y no me preguntéis: no lo voy a contar.
Para esta demostración he creado una aplicación MFC mediante el asistente de
Visual Studio, que se puede bajar de aquí. Es una aplicación cuya ventana
principal es un cuadro de diálogo, y vamos a ignorar todos los asistentes y a
modificar a mano todo el código.
En primer lugar, abrimos la plantilla del cuadro de diálogo y añadimos un botón
que vamos a llamar “Cambiar”, tal y como se muestra en la imagen.
Como no nos interesan los ID, nos da igual qué ha hecho el diseñador. El
siguiente paso consiste en asociar cada uno de los cuatro controles a una clase,
por lo que en primer lugar tenemos que crearnos cuatro variables del tipo
adecuado en el fichero cabecera de la clase:
CButton btnOk;
CButton btnCancel;
CButton btnCambiar;
CStatic label;
Ahora nos vamos al método OnInitDialog() y añadimos la línea
EnumChildWindows(m_hWnd,(WNDENUMPROC)staticEnumChildProc,(LPARAM)this);
Que es el núcleo de todo el asunto que nos ocupa. Claramente estamos utilizando
una función de Win32 que mediante otra función de callback nos irá enumerando
todas las ventanas hijas. m_hWnd es la variable miembro de la clase
diálogo que contiene el Handle de ventana. El parámetro siguiente es la función
de callback que irá siendo llamada ante cada ventana hija que se encuentre.
Luego explico el tercer parámetro, que es algo que podemos pasarle al enumerador
de ventanas.
Debemos ahora crearnos una función estática dentro de la clase con la firma
adecuada para que el código de EnumChildWindows() pueda llamarla. Dicha
función ha de ser estática porque Win32 no entiende de punteros this y
otras zarandajas y, o usamos una función global, o una estática dentro de la
clase.
Pero esto tiene un problema, ya que desde dicha función no podremos llamar a
ningún método miembro ni tocar ninguna variable miembro, y es aquí donde surge
el tercer parámetro de EnumChikdWindows(): le estamos pasando un puntero
a la propia clase, para que la función estática pueda ejecutar el {
return ((CmfcDialogNoIDDlg *)lParam)->EnumChildProc(hwnd);
}tancias de la misma
clase sin preocuparnos de esa fea función estática, y es una de varias formas
posibles de hacerlo. Veamos el código, que he añadido en el fichero cabecera:
static BOOL CALLBACK staticEnumChildProc(HWND hwnd,LPARAM lParam)
{
return ((CmfcDialogNoIDDlg *)lParam)->EnumChildProc(hwnd);
}
Y finalmente el código que asocia cada control a su clase correspondiente:
BOOL EnumChildProc(HWND hwnd)
{
TCHAR szTitle[255];
if(::GetWindowText(hwnd,szTitle,255)!=0)
{
if(_tcscmp(szTitle,TEXT("OK"))==0)
btnOk.Attach(hwnd);
else if(_tcscmp(szTitle,TEXT("Cancel"))==0)
btnCancel.Attach(hwnd);
else if(_tcscmp(szTitle,TEXT("TODO: Place dialog controls here."))==0)
label.Attach(hwnd);
else if(_tcscmp(szTitle,TEXT("Cambiar"))==0)
btnCambiar.Attach(hwnd);
}
return TRUE;
}
El código está claro: ante cada handle de ventana que recibimos, buscamos su
título y dependiendo de cuál sea, se asocia al control adecuado.
Ahora tan sólo nos falta crear un evento de respuesta ante el mensaje
WM_DESTROY y desasociar los controles para evitar que el programa proteste.
Mediante el uso de la ventana de propiedades del cuadro de diálogo, creamos un
nuevo evento y lo rellenamos con el código
void CmfcDialogNoIDDlg::OnDestroy()
{
CDialog::OnDestroy();
// TODO: Add your message handler code here
btnCancel.Detach();
btnOk.Detach();
label.Detach();
btnCambiar.Detach();
}
Si ahora ponemos un punto de interrupción dentro de EnumChildProc()
veremos cómo se van asociando todos los controles a cada uno de los objetos
adecuados.
Ejecutar eventos
Tan sólo nos queda poder ejecutar un código para el botón de “Cambiar” y que
cambie los textos de todos los controles. Para ello tenemos que bucear un rato
dentro del código fuente de MFC para encontrar la forma, ya que casi como
siempre, la documentación está, pero hay que saber dónde encontrarla, así que lo
más sencillo en este caso es mirar el código fuente y pensar un poco.
En un cuadro de diálogo los mensajes de los controles hijos se suelen controlar a
nivel del padre, es decir, es la propia clase del diálogo, la que, mediante los
mapas de mensajes, se encarga de reaccionar a cualquier evento de los hijos.
Una mirada al método WindowProc() de la clase CWnd nos da la pista:
cuando se trata de un mensaje WM_COMMAND (lo generados por los controles
hijos, entre otros), se controlan mediante el método virtual OnCommand().
Ahora sí, ya lo tenemos claro, debemos sobrescribir este método virtual y
reaccionar con nuestro código. Un vistazo a la documentación nos lo aclara:
lParam es el handle de ventana mientras que parte de wParam es el ID del
control. Ya sabemos cómo reaccionar, por lo que añadimos el siguiente código al
fichero cabecera:
virtual BOOL OnCommand(WPARAM wParam,LPARAM lParam)
{
if((HWND)lParam==btnCambiar)
{
btnOk.SetWindowText(TEXT("¡Era OK!"));
}
return CDialog::OnCommand(wParam,lParam);
}
Comprueben el código de ejemplo. Y hemos terminado.
Ir al índice de los artículos de RFOG en el sitio del Guille