miércoles, septiembre 08, 2010

Cómo espiar los mensajes de un control

Esto es lo que se ve al espiar ciertas ventanasSigo con trucos necesarios para implementar un sistema de gestión de ventanas más o menos complejo. Imagine que tiene un botón, y que debe responder a los clics del mismo. Ni lo piense: desde la ventana, o desde la clase que contiene al botón, añade un manejador al evento Click del botón. Esta es una ventaja que tiene .NET y su sistema de eventos multicast sobre el viejo y más sencillo sistema de eventos de Delphi nativo. En Delphi nativo solamente podíamos asociar un único manejador a cada evento. En .NET, el evento puede disparar cuantos manejadores necesitemos.
La gran pregunta: ¿y qué ocurre si lo que quiero interceptar es un mensaje que no tiene un evento de "alto nivel" asociado? La solución "clásica" es crear una clase derivada a partir del botón. Pero, ¿y si no es posible hacerlo? Por ejemplo, puede que le toque lidiar con un botón que usted no ha creado (la culpa siempre es de otro). En mi caso, no se trata de un mero botón, sino de la mismísima ventana principal. Estoy escribiendo un gestor genérico de ventanas, y necesito interceptar el mensaje WM_ACTIVATEAPP. Podría obligar al programador que utilice mi gestor a derivar su ventana principal de una clase base mía, debidamente retocada. Sería chapucero, no obstante.
Este es un problema bastante general del modelo de programación orientada a objetos que terminó imponiéndose en nuestros lenguajes de programación: el tipo de objeto va unido indisolublemente a su entidad. Un coche de turismo no se puede convertir dinámicamente en una ambulancia... aunque en la vida real, los turismos puedan utilizarse así. Este problema hace complicado trabajar con bases de datos orientadas a objetos cuyo modelo de objetos sea parecido al de C++, Java o C#. No me malinterprete: el modelo con tipos estáticos es estupendo para escribir cierto tipo de programas, pero es malo para las bases de datos, en las que un objeto de la clase Contacto puede fácilmente en un Cliente, y no podemos perder las referencias al contacto durante la metamorfosis.
Lo interesante es que existe una solución muy conocida para este problema... en el API de bajo nivel de Windows; si busca window subclassing encontrará montones de páginas sobre esta técnica. La pregunta es, ¿cómo conseguir lo mismo utilizando Windows Forms?
La respuesta la tiene la clase NativeWindow, que nos evita tener que usar PInvoke o cosas peores. ¿Hay que interceptar los mensajes que recibe un control? Pues creamos una clase derivada de NativeWindow, sobrescribimos su método virtual WndProc y, para asociarla al puñetero botón, usamos el método AssignHandle de NativeWindow para copiar en una instancia de la nueva clase el identificador de ventana del botón a vigilar. Los mensajes de ventana que normalmente recibiría el botón serán ahora recibidos también por nuestra instancia de la clase derivada de NativeWindow.
Es cierto que seguimos necesitando crear una clase por herencia. Si necesitamos realizar esta operación de vigilancia con N tipos diferentes de botones, ¿significa que tendremos que crear N clases derivadas de NativeWindow? Resulta que no: podemos definir una sola clase derivada de NativeWindow sin tener conocimiento del control concreto que vigilaremos y las acciones concretas que queremos ejecutar. Lo que haríamos en su WndProc, sencillamente, sería llamar a una variable de tipo delegate que especificaríamos al crear las instancia de dicha clase. Y sería ese delegado donde implementaríamos el código necesario:
private sealed class PeepingTom : NativeWindow, IDisposable
{
public delegate void WindowProc(ref Message m);

private WindowProc tommy;

public PeepingTom(Control ladyGodiva, WindowProc tommy)
{
this.tommy = tommy;
this.AssignHandle(ladyGodiva.Handle);
}

public void Dispose()
{
this.ReleaseHandle();
}

protected override void WndProc(ref Message m)
{
tommy(ref m);
base.WndProc(ref m);
}
}
Los nombres, por supuesto, hacen referencia a la historia de Lady Godiva, que se paseó desnuda a caballo por todo Coventry para obligar a su marido a bajar los impuestos (¡que no se le ocurra la idea a la Sonsoles, please!), y a un tal Peeping Tom, o Tomasito el Mirón, que desobedeciendo las órdenes del cabreado marido, se escondió para regocijarse con la visión de la damita en pelotas.
Suponga ahora que tenemos un formulario con un botón, y que queremos saber cuándo el ratón pasa por encima del botón. Para asignarle un espía al botón, dado el diseño de nuestra clase, tenemos que esperar al evento Load del formulario, para asegurarnos de que el identificador de ventana del botón haya sido creado. En ese momento, hacemos algo parecido a esto:
private void Form1_Load(object sender, EventArgs e)
{
new PeepingTom(button1,
delegate(ref Message m)
{
if (m.Msg == 0x02A1)
this.Text = "Mouse hover";
else if (m.Msg == 0x02A3)
this.Text = "Mouse leave";
});
}
El ejemplo no es muy bueno, porque para esta tarea en particular, ya tenemos eventos en la clase Button, pero espero que sirva para que se haga una idea.

Etiquetas: , , ,

sábado, septiembre 04, 2010

Una ventana dentro de otra

La Condition Humaine, de René MagritteEste es un truco rápido y sencillo que, sin embargo, es difícil encontrar a la primera buscando en Internet:
  • ¿Cómo meter una ventana dentro de otra?
¿Y para qué le puede hacer falta tal cosa? Un uso evidente sería crear un sistema de docking, que permita agrupar dinámicamente varias ventanas dentro de un contenedor; naturalmente, saber cómo meter una ventana independiente dentro de otro control es sólo el comienzo del proceso.
Pero existe otro uso más mundano: imagine que está creando un administrador de ventanas. Teóricamente, el administrador debe permitirle cambiar el estilo de la aplicación a petición del usuario: de ventanas SDI individuales, a una interfaz basada en pestañas e incluso a un sistema MDI. ¿Cómo deberían implementarse las ventanas para que pudiesen utilizarse con este gestor? Una posibilidad, que he utilizado en varios proyectos, es que las "ventanas" se diseñen como controles de usuarios (UserControl). El motivo es lo fácil que se añade un control dentro de otro, ya sea en tiempo de diseño o de ejecución.
Sin embargo, no es tan evidente cómo meter una instancia de la clase Form dentro de, digamos, un TabPage. Si lo intenta, sin más, provocará una excepción advirtiendo que "no se puede meter un control de nivel superior dentro de otro". El error, por fortuna, nos indica también la solución. Hay una propiedad poco conocida de la clase Form, que hay que poner a false. Por ejemplo, si queremos meter una instancia de la clase Form2 dentro de una nueva página de un TabControl, necesitaremos algo parecido a esto:
var f2 = new Form2();
f2.TopLevel = false;
f2.FormBorderStyle = FormBorderStyle.None;
f2.Dock = DockStyle.Fill;
var tp = new TabPage(
tabControl1.TabPages.Count.ToString());
tabControl1.TabPages.Add(tp);
tp.Controls.Add(f2);
f2.Visible = true;
Aquí he metido la ventana, que puede haber sido diseñada al antojo del programador, dentro de un control de pestañas. Pero es igualmente posible meterla directamente dentro de un prototipo vacío de ventana hija MDI... o lo que se le ocurra.

Etiquetas: , ,