Control de mensajes del sistema mediante subclasificación y la interfaz IMessageFilter

 

Marino Posadas
Netalia
www.ElAveFenix.net

Antes de la llegada de .NET, cualquier programador que desease acceder a las características reservadas del sistema operativo, tenía que profundizar en las API’s de Win32, en busca de las funciones adecuadas, y proveer a su aplicación de los mecanismos de llamada seguros que le permitiesen comunicarse con unos recursos que –de por sí- están diseñados en C++.

Esto suponía una gran esfuerzo para cualquiera que -simplemente- buscaba acceder a un conjunto funcional, sin el galimatías de pasar por interminables API y el tiempo que eso conlleva. Hoy día, cuando, en cuestion de minutos, navegar por cualquier información, desde comprobaciones del sistema hasta http://espanol.partycasino.com/ es algo trivial, nos resulta extraño que en la época previa a .NET se tratase casi de una misión imposible. Así que, con su aparición, .NET presentaba un panorama más brillante y optimista.

Todo ello, además, conlleva una atención especial a los tipos de datos y las conversiones entre ellos, ya que, de otro modo, los errores pueden ser catastróficos. En .NET, muchas de esas llamadas al API han sido puestas directamente a disposición del programador dentro de la inmensa jerarquía de clases que contiene, ofreciendo una solución simple a problemas que antes resultaban complejos y delicados de tratar.

La subclasificación antes de .NET

Los programadores de Visual Basic, acogieron con alegría la aparición del operador unario AddressOf en VB5, ya que permitía el acceso –vedado hasta ese momento- a todo un mundo de intercomunicación con el sistema basado en el concepto de callback (retro-llamada). Las retro-llamadas están el núcleo mismo de muchos de los servicios que aporta el sistema, y se basan en la idea de indicar a Windows que el resultado de una llamada al sistema, en lugar de ser devuelto directamente por una función, sea notificado al usuario mediante una llamada (por parte del sistema mismo) a una función que el programa haya creado a tal efecto.

Típicamente, esa situación se produce en dos situaciones concretas: cuando la llamada al sistema de forma síncrona pudiese bloquear recursos o dejar a la aplicación “colgada”, en espera de la respuesta, o bien cuando lo que se desea es sustituir un comportamiento predeterminado del sistema. Podemos ver esta situación en el siguiente diagrama:



Figura 1: Envio y recepción de mensajes a una ventana

Un ejemplo del primer caso, sería cuando pedimos a una ventana MDI que enumere sus ventanas hijas (función EnumChildWindows()). El otro caso, se produciría cuando queremos que un comportamiento como mostrar un menú al pulsar botón derecho sobre el objeto, sea modificado.

Mensajes Hardware y Software del sistema operativo

Cada vez que el usuario interactúa con un elemento del hardware como el ratón o el teclado, o bien cuando el sistema mismo recibe información de cualquier tipo que debe de procesar (digamos, por ejemplo, por un puerto de comunicaciones), genera un mensaje dirigido al objeto destinatario de esa información. Se produce así la transformación del evento hardware en un evento software.

Por otro lado, como la cantidad de mensajes que se pueden generar es enorme, todo el sistema funciona de forma asíncrona. Se genera el mensaje, se envía a una Cola de Mensajes (Window Message Queue), asociada a cada objeto receptor y –a medida que el sistema se lo permite- la cola va siendo procesada mediante un Despachador de Mensajes, que toma las acciones adecuadas para cada uno de ellos: así cuando movemos una ventana, seleccionamos el botón de minimizar, o presionamos ALT+F4 para cerrarla, la ventana receptora recibe las instrucciones mediante este mecanismo, y actúa en consecuencia.

El concepto de subclasificación se basa entonces en sustituir el despachador de mensajes, llamado técnicamente procedimiento o función de ventana (y abreviado WndProc), por una función de usuario que reciba el mensaje y determine si quiere realizar alguna acción: si es así, la acción se programa como se prefiera, y se le indica a Windows que el mensaje ha sido procesado. O mejor todavía, se trabaja de forma intercalada, esto es, procesamos el mensaje previamente, y luego llamamos al procedimiento de ventana estándar para que éste haga lo que corresponda en cada caso.

Desde el punto de vista programático, en versiones anteriores había que manejar varias API’s del sistema, averiguar el procedimiento de ventana del objeto en cuestión y utilizar el operador AddressOf para indicar al sistema cuál era nuestra propia función de ventana (que hacía las veces de cortafuegos entre el sistema y el objeto).

En .NET, disponemos de dos modos de hacer esto, con diferentes medios, aunque similares capacidades: podemos sobrescribir el procedimiento de ventana de un objeto mediante técnicas de OOP (lo que circunscribe nuestro trabajo a un objeto dado), o podemos utilizar una clase que implemente la interfaz IMessageFilter, para controlar esa situación a nivel de aplicación.

Veamos la implementación de estas características, comenzando por un objeto TextBox al que queremos anular el comportamiento predeterminado asociado al botón derecho:

public class CajaSinMensajes: System.Windows.Forms.TextBox
{
    protected override void WndProc(ref Message m)
	{
	base.WndProc(ref m);
		switch(m.Msg)
		{
	 	case (int) Mensajes.WM_RBUTTONDOWN:
			MessageBox.Show("Botón derecho deshabilitado");
			break;
	 	case (int) Mensajes.WM_LBUTTONDOWN:
			MessageBox.Show("Botón izquierdo deshabilitado");
			break;
		}
	return;
} 
}

 

Los mensajes enviados por Windows tienen todos una cuaterna de argumentos: el manejador de la ventana a quien va dirigido (un valor int que es único en el sistema, e identifica cualquier objeto, al que se suele hacer referencia como hWnd), el contenido del mensaje en sí (otro valor int, llamado aquí Msg, que se corresponde con la acción a realizar, y dos argumentos adicionales (llamados WParam y LParam), que sirven de parámetros modificadores del mensaje si el tipo de mensaje lo requiere (por ejemplo al mensaje mover la ventana, se le pasan las coordenadas).

Como puede verse, la clave consiste en sobrescribir el método correspondiente al procedimiento de ventana, para lo que creamos una clase que herede de TextBox (CajaSinMensajes), y simplemente, añadimos el método WndProc tal y como está definido en su clase antecesora, teniendo cuidado de llamar previamente al procedimiento de ventana predeterminado, e incluyendo después el comportamiento que queramos.

Al objeto de que el lector pueda experimentar con ésta característica, se ha incluido en el proyecto una clase tipo Enum, que recoge varios cientos de mensajes típicos de Windows. De ahí que la comparación del valor de la propiedad Msg del objeto mensaje (m) sobre dos valores de dicha clase.

En el ejemplo, hemos dispuesto dos controles Textbox en un formulario. El primero, de nuestra clase CajaSinMensajes, y el segundo estándar. El segundo tiene un comportamiento normal, pero cuando intentamos usar el ratón sobre el primero, detectará el botón pulsado y responderá adecuadamente. Además del código anterior, simplemente hemos declarado los dos controles de la siguiente forma:

private System.Windows.Forms.TextBox textBox2;
private MensajesWindows.CajaSinMensajes textBox1;

 

siendo MensajesWindows el nombre del namespace correspondiente a nuestra aplicación.



Fig. 2: Ejecución del código anterior

La misma técnica podría haberse utilizado sobre toda la ventana, por ejemplo, para impedir que el usuario pueda moverla, con sólo cambiar el mensaje capturado por uno del tipo Mensajes.WM_MOVE. La única diferencia significativa en este caso, es que no necesitamos crear un formulario que herede de Form, sino que basta con sobrescribir directamente el método WndProc().

En busca de una solución global: la interfaz IMessageFilter

En ocasiones, sin embargo, la solución anterior no basta. Imaginemos que estamos en un escenario en el que debe ser capturado cualquier mensaje de un tipo que vaya dirigido a cualquier control del formulario incluido el propio formulario (pongamos que sea el botón derecho, solamente).

En tal caso, necesitamos una clase que monitorice toda la actividad de envío de mensajes e intervenga anulando la funcionalidad (o añadiendo la que se requiera), sin importar a quién vaya dirigida. La solución está en crear una clase que implemente la interfaz IMessageFilter.

El encargado de la tarea es el objeto Application, que –lo mismo que se encarga de asignar el bucle de mensajes Windows a un formulario, mediante su método Run-, puede interceptar todos los mensajes dirigidos a ese objeto y sus contenidos, mediante su método AddMessageFilter().

Ahora bien, para poder utilizarlo necesitamos crear una instancia una clase que soporte dicha interfaz. Si nos fijamos en la definición que se da de dicha interfaz en la documentación, veremos que exige a la clase que la implemente un único método PreFilterMessage, definido como sigue:

Como podemos ver, la técnica es similar a la anterior, pero, además de actuar sobre todos los mensajes de cualquier ventana (no olvidemos que un control es una ventana), debemos indicar a Windows si el mensaje está procesado o no, mediante el valor de retorno. De forma que implementaremos una clase paralela a la de nuestro formulario, de la siguiente forma:

class FiltroRatón:IMessageFilter //controla los mensajes de botón derecho
	{
		public bool PreFilterMessage(ref Message m)
		{
			if (m.Msg == (int) Mensajes.WM_RBUTTONDOWN)
			{
				MessageBox.Show("Botón derecho desahabilitado");
				return(true);
			}
			return(false);
		}
	}

Y antes de lanzar la aplicación, añadiremos una instancia de dicha clase al objeto Aplplication, mediante la inclusión de una línea extra en el código fuente:

static void Main() 
{
		// Primero, añadimos un filtro de control al objeto Application
		Application.AddMessageFilter(new FiltroRatón());
		// Ahora asignamos el bucle principal de eventos a 
		// una instancia de Form1
		Application.Run(new Form1());
	}

 

Y ya está. Cualquier pulsación de botón derecho, independientemente del control sobre el que se realice, será capturada, indicando a Windows que la labor asociada a ese mensaje ha sido procesada, por lo que no tiene ninguna tarea pendiente. En caso contrario, devolvemos el valor false, para que Windows trabaje en forma predeterminada.

Marino Posadas
Grupo EIDOS
www.ElAveFenix.net

-------BIO--------

Marino Posadas es MVP en .NET Framework, MCSD, MCAD y MCT y suele colaborar en los Eventos y Talleres de Desarrolladores Microsoft organizados por Microsoft España, habiendo participado como conferenciante distintos eventos en EE.UU y América Latina. Es autor de 6 obras sobre programación (la última, un e-Book sobre programación en C#), y numerosos artículos en varias revistas españolas.