jueves, 27 de agosto de 2009

C#, Visual Basic 6 y el HWND de la ventana principal…

Hola! ¿Como ha ido el verano? A todos los que hayais disfrutado de unas buenas vacaciones, espero que os hayan sido provechosas…

Pero como dicen, todo lo bueno se acaba, y toca volver al tajo. En el proyecto en el que estoy, nos hemos visto en la necesidad de comunicarnos con la ventana principal de otro proceso, realizado en Visual Basic 6.

Sobre ventanas, HWNDs y demás…

Los que conozcais un poco como funciona internamente Windows, podéis saltaros este apartado y pasar directamente al siguiente ;-) Para el resto una pequeña introducción.

Todos tenemos claro lo que es la ventana de un programa, pero en Windows el concepto de ventana es mucho mas amplio: todo, absolutamente todo lo que se ve en pantalla es, de alguna manera u otra una ventana. Un botón? Es un tipo de ventana específico. Una check-box? Es una ventana. Una label? Es otra ventana, y así con todo lo que os podáis imaginar. E incluso más: los procesos crean muchas ventanas ocultas y que nunca jamás se llegan a mostrar.

¿Para que le puede interesar a un  programa crear una ventana que no se muestre nunca? Pues porque las ventanas son ni más ni menos que el método de comunicación de Windows. Cuando el sistema operativo debe informar de un evento, lo que hace es enviar un mensaje a los procesos que deben enterarse de dicho evento. Cada proceso recoje el mensaje y lo envía a la ventana que corresponda. Por suerte .NET se encarga de todo esto (y mucho más) de forma automática, pero si os pica la curiosidad como funciona el proceso echadle un vistazo a métodos del Api de Win32 como PeekMessage, GetMessage o DispatchMessage. Las dos primeras son usadas para recojer mensajes de la cola de mensajes del proceso, y la tercera para enrutarlos a la ventana correspondiente.

Cada ventana que se cree (recordad: cada botón, cada label, todo) tiene su procedimiento de ventana, que define como actua esta ventana. Si el botón informa de los clicks que se hacen en él, es porque hay código específico en el su procedimiento de ventana para que esto ocurra.

Así una aplicación básica Windows consta de dos elementos fundamentales: El llamado bucle de mensajes, que se encarga de recoger todos los mensajes que Windows envía al proceso) y varios procedimientos de ventana que se encargan de procesar dichos mensajes. La clásica función main() de una aplicación Windows tradicional contiene el bucle de mensaje, y el resto de código está repartido en los distintos procedimientos de ventana.

Para comunicar partes de nuestro programa usamos siempre mensajes: desde el procedimiento de ventana de una ventana, podemos enviar mensajes a cualquier otra ventana usando funciones del API como SendMessage o PostMessage. Por eso usamos muchas veces ventanas ocultas: simplemente para poder recibir mensajes y poderlos procesar con código propio.

Cada ventana que se crea tiene un identificador único que se conoce como el HWND de la ventana. Las ventanas también tienen una clase. Aunque también define otras cosas podemos asumir que la clase de una ventana determina que procedimiento de ventana se usará: es una manera de que vayas ventanas distintas compartan un mismo procedimiento de ventana. Así p.ej. existe una clase llamada “Button” que define el procedimiento de ventana de todos los botones de Windows. Si quereis profundizar un poco sobre el tema, os recomiendo este artículo: Window Classes in Win32. Evidentemente nosotros podemos definir clases nuevas que es lo que debemos hacer cuando queremos tener un procedimiento de ventana propio.

Así que, para resumir: En Windows casi todo son ventanas, que se comunican enviando y recibiendo mensajes (que son gestionados por el procedimiento de ventana) y cada ventana se referencia por su único HWND.

Visual Basic 6 y la “ventana principal”

Generalmente para encontrar la ventana principal de un proceso, podemos usar directamente la propiedad MainWindowHandle de la clase Process, que nos devuelve un IntPtr con el HWND de la ventana principal. Una vez tenemos el HWND podemos hacer, literalmente, lo que nos de la gana con esta ventana.

Peeeeeeeeeeeeero, resulta que cuando se ejecuta un programa en VB6 se crean siempre al menos dos ventanas:

  1. Una ventana visible (aunque con tamaño 0x0) cuya clase es ThunderRT6Main (nota freak: Thunder es el nombre en clave de VB).
  2. Otra ventana visible, cuya clase es ThunderRT6FormDC y que es en efecto el formulario principal.

Esto es así incluso para el más simple programa en VB6 que muestre una ventana: un formulario vacío que sea el “Startup Object” del proyecto VB6.

image

La captura de pantalla está sacada Spy++, una herramienta que viene con Visual Studio desde tiempos inmemoriales y que sirve para ver todas las ventanas que hay creadas en un momento dado en el sistema. He marcado en rojo la ventana ThunderRT6Main y en azul la ventna ThunderRT6FormDC. El dialogo “Property Inspector” se corresponde a la ventana “Formulari sense Main” que es el formulario principal de la aplicación. Podemos ver dos cosas:

  1. La ventana ThunderRT6Main es la propietaria de la ventana ThunderRT6FormDC…
  2. … pero esta no es su hija: ambas son hijas del Desktop (o ambas son “ventanas principales” por lo que respecta a Windows). Si observais el “Property Inspector” el handle de la “Owner Window” es el mismo handle que la ventana ThunderRT6Main.

¿Y cual es el handle que nos devuelve Process.MainWindowHandle? Bueno… pues el nombre de las clases de ambas ventanas ya lo aclara un poco, no? Es el handle de la ventana ThunderRT6Main, una ventana cuyo tamaño es 0x0 y que realmente no vemos (esto se puede observar también con Spy++).

Entonces como obtener el handle a la ventana principal “real”? Esa es la necesidad que tenía yo en mi proyecto. Una solución es usar EnumThreadWindows: esta función itera por todas las ventanas principales creadas por un thread determinado, llamando a una función de callback por cada función. Y que podemos hacer en la función callback? Pues encontrar su clase (llamando a GetClassName) y mirar si es igual a ThunderRT6FormDC.

El código a grandes rasgos sería como sigue:

// Delegate para EnumThreadWindows
delegate bool EnumThreadWndProc(System.IntPtr hwnd, IntPtr lParam);

class Program
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetClassName(IntPtr hWnd,
StringBuilder lpClassName, int nMaxCount);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool EnumThreadWindows
(uint dwThreadId, EnumThreadWndProc lpfn, IntPtr lParam);

static void Main(string[] args)
{
Process process = new Process();
// NoMainProject es un proyecto simple de
// VB6 que muestra un form
process.StartInfo = new ProcessStartInfo(@"C:\NoMainProject.exe");
process.Start();
// Damos tiempo al proceso VB6 de ponerse en marcha
Thread.Sleep(1500);
// Enumeramos las ventanas del thread principal
EnumThreadWindows((uint)process.Threads[0].Id,
FindVB6Window, IntPtr.Zero);
Console.ReadLine();
}

// Encuentra la venana cuya clase sea ThunderRT6FormDC
static bool FindVB6Window(IntPtr hwnd, IntPtr lParam)
{
StringBuilder sb = new StringBuilder(1024);
// Obtenemos el nombre de la clase
GetClassName(hwnd, sb, sb.Capacity);
string className = sb.ToString();
if (className.Equals("ThunderRT6FormDC"))
{
Console.WriteLine("HWND ventana ppal 0x{0:X} ",
hwnd.ToInt32());
return false;
}
return true;
}
}

Y una captura de la salida, junto con la verificación con Spy++:


image


Evidentemente hay casos más compejos (p.ej. nuestra aplicación VB6 podría crear dos formularios y mostrar ambos) pero la idea de fondo sería la misma (aunque entonces debemos utilizar algún mecanismo adicional además del nombre de clase para discernir cual es la “principal”).


Saludos!!! :)