Moviendo ventanas con la API de Windows, la historia de WinResize

Para hacer pruebas de VS Anywhere en un entorno de producción, una de las cosas que solemos hacer es abrir 2 sesiones de Visual Studio a cada lado de la pantalla, de esta manera podemos hacer pruebas rápidas de características específicas.

goal

El problema es que esto requiere abrir 2 instancias de visual studio de manera manual, y fijarlas cada una a un lado de la pantalla, algo tedioso, aburrido, y, sobre todo, automatizable. Así que me lancé a la búsqueda de la verdad, qué me puede solucionar?

El camino recorrido

Si tenemos Windows 7 o superior podemos hacer uso de Aero Snap, que usando Win+Izquierda o Win+Derecha, fija una ventana a la izquierda o a la derecha de la pantalla. Parecía sencillo, solamente tendríamos que emular esta combinación de teclas, lo cual, en un principio era algo fácil de hacer:

En teoría, usando Powershell, podríamos hacerlo, de hecho, en la siguiente entrada de TechNet Provide Input to Applications with PowerShell se explica cómo enviar comandos a una aplicación empleando SendKeys, siendo este código el resultado:

add-type -AssemblyName microsoft.VisualBasic
add-type -AssemblyName System.Windows.Forms
Calc
start-sleep -Milliseconds 500
[Microsoft.VisualBasic.Interaction]::AppActivate("Calc")
[System.Windows.Forms.SendKeys]::SendWait("1{ADD}1=")

De acuerdo con la última línea, deberíamos poder enviar al programa cualquier combinación de teclas, así que el siguiente paso era buscar la tecla Windows en la referencia de SendKeys: http://msdn.microsoft.com/en-us/library/system.windows.forms.sendkeys.send(v=vs.110).aspx

Sorpresa: no está. Por otra parte, buscando un poco por internet, encontré que se podría simular la pulsación de la tecla Windows usando la  Ctrl + Esc.

Segunda sorpresa: SIMULA LA TECLA WINDOWS, y nada más. En el momento que se pulsa eso, se salta al escritorio, o al menú inicio, no siendo posible usarlo para simular una combinación de teclas que involucre Win + cualquier cosa, así que por este camino poco más podemos hacer.

Por otro lado, si lo que estamos haciendo es un atajo de teclado, seguramente corresponda con alguna API de windows que haga la acción de Aero Snap, pero no, no hay API, no hay documentación y no hay ninguna referencia salvo el nombre comercial. con lo cual este es otro callejón sin salida,

Aún quedaba una opción, que era mover directamente la ventana usando código, tras algo de búsqueda encontré este código, que permitía hacer lo que quería usando powershell:

https://gist.github.com/coldnebo/1148334

No era demasiado complejo, pero opté por hacer una pequeña aplicación C# para que me resolviera el problema, usando las llamadas nativas a la API de Windows a través de P/Invoke:

Importando las funciones y moviendo la ventana

Para usar funciones de la API de Windows hemos de copiar su cabecera en nuestra función, definirla como externa (el framework ya sabrá que hacer) y definir la DLL de la que realizaremos la importación, en este caso la función MoveWindow situada en user32.dll:

[DllImport("user32.dll")]
public static extern bool MoveWindow(IntPtr hWnd, int X, 
int Y, int nWidth, int nHeight, bool bRepaint);

Deberemos repetir este proceso para cada función que queramos importar, y además, para todo esto, necesitaremos agregar el siguiente using:

using System.Runtime.InteropServices;
Para poder mover la ventana, necesitamos su hWnd, que es un identificador de ventana único. Hay muchas maneras de localizarlo, y una de ellas es a partir de la lista de procesos, seleccionando aquellos cuya ventana principal coincidiera con lo que estaba buscando, en este caso Visual Studio.

foreach (Process proc in Process.GetProcesses())
{
    if (proc.MainWindowTitle.Contains("Visual Studio"))
    {
        IntPtr handle = proc.MainWindowHandle;
        ...
        MoveWindow(handle ...);
        ...
    }
}

Para poder mover la pantalla necesitamos establecer un punto origen y un tamaño, y para ello podríamos o bien fijarlos manualmente, o basarnos en la resolución de la pantalla, usando para ello las siguientes funciones:

Screen.PrimaryScreen.WorkingArea.Width;
Screen.PrimaryScreen.WorkingArea.Height;
que se encuentran dentro del namespace System.Windows.Forms, que requiere además una referencia as System.Drawing desde nuestro proyecto.

Lo realmente interesante de esto, es que me permitía mover la ventana a la posición que quisiera con cualquier tamaño, así que lo aproveché para poder establecer cualquier número de ventanas, estando estas igualmente distribuidas.

Finalmente necesitamos un par de llamadas más a funciones de la API de Windows para maximizar la ventana antes de moverla, y para establecer el foco, que se importan de manera similar:

[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int X);
[DllImport("user32.dll")]
public static extern bool SetFocus(IntPtr hWnd);

Lanzando las aplicaciones y parámetros de entrada

Además de la funcionalidad de mover ventanas, podría ser interesante lanzar las aplicaciones, así que un poco de código para generar un proceso, establecer el nombre del fichero, y, tras lanzarlo, una leve espera mientras carga:
Process p = new Process();
p.StartInfo.FileName = "notepad.exe";
p.Start();
System.Threading.Thread.Sleep(2000);
Para hacerlo más sencillo, el último paso es convertir todos los parámetros usados a parámetros de entrada, siendo finalmente el proyecto una aplicación de consola, que recibe, en este orden;
  • Ruta del ejecutable
  • Título de la ventana a buscar
  • Número de procesos a lanzar
  • Espera para cada proceso
La salida que muestra la aplicación de consola es ésta, para el caso de 3 ventanas de notepad.exe:

WinResize

Por otra parte tenemos las 3 ventanas, igualmente distribuidas.

Result

Como último detalle, el título de la ventana está fijado usando el siguiente bloque de código:

Console.Title = "WinResize";

Conclusiones

Esta es una pequeña app que en mi caso soluciona un escenario muy concreto, se puede expandir hasta límites insospechados, y posiblemente lo haga, aunque hay cosas que se me han quedado en el tintero seguro, y que dejo como idea para el futuro:
  • Hacer todo el script en powershell, es posible, y podemos hacer P/Invoke sin problemas
  • Emular la pulsación de la tecla (tiene que haber un código asociado a la tecla windows, y una manera de pulsarlo!) simulando el teclado.
  • En vez de recorrer todos los procesos, usar el identificador de los que acabo de crear.
El código está disponible de manera gratuita y bajo licencia open-source. Si quieres contribuir envía tu pull request :)

Enjoy!

Enlaces adicionales