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.
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Win7 Powershell script to resize Minecraft to 1280×720 (HD for fraps youtube capture) | |
# use by typing the following at a command prompt: | |
# > PowerShell -ExecutionPolicy Bypass -File minecraft-sethd.ps1 | |
# refs: | |
# http://stackoverflow.com/questions/2556872/how-to-set-foreground-window-from-powershell-event-subscriber-action | |
# http://richardspowershellblog.wordpress.com/2011/07/23/moving-windows/ | |
# http://www.suite101.com/content/client-area-size-with-movewindow-a17846 | |
Add-Type @" | |
using System; | |
using System.Runtime.InteropServices; | |
public class Win32 { | |
[DllImport("user32.dll")] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); | |
[DllImport("user32.dll")] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); | |
[DllImport("user32.dll")] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint); | |
} | |
public struct RECT | |
{ | |
public int Left; // x position of upper-left corner | |
public int Top; // y position of upper-left corner | |
public int Right; // x position of lower-right corner | |
public int Bottom; // y position of lower-right corner | |
} | |
"@ | |
$rcWindow = New-Object RECT | |
$rcClient = New-Object RECT | |
$h = (Get-Process | where {$_.MainWindowTitle -eq "minecraft"}).MainWindowHandle | |
[Win32]::GetWindowRect($h,[ref]$rcWindow) | |
[Win32]::GetClientRect($h,[ref]$rcClient) | |
$width = 1280 | |
$height = 720 | |
$dx = ($rcWindow.Right – $rcWindow.Left) – $rcClient.Right | |
$dy = ($rcWindow.Bottom – $rcWindow.Top) – $rcClient.Bottom | |
[Win32]::MoveWindow($h, $rct.Left, $rct.Top, $width + $dx, $height + $dy, $true ) |
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:
Por otra parte tenemos las 3 ventanas, igualmente distribuidas.
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
- En Github:Código fuente de la aplicación
- En Stack Overflow: Programmatically Maximize Window On Half Of Screen
- En Stack Overflow: Get the handle of a window with not fully known title. (C#)
- En MSDN: Console.Title
- En MSDN: SendKeys.Send
- En Github: Resizing a Minecraft Window
Y meterlo como un PlugIn dentro del IDE (uds de eso saben) de manera que lo haga solo cuando se inicia ;) … ahi te dejo la idea :D
Buenas noches Rober,
He encontrado por internet un proyecto que ayuda un poco a la hora de utilizar teclas:
http://inputsimulator.codeplex.com/
Con este proyecto, una vez configurado el foco a la ventana que queramos podemos hacer lo siguiente para ponerla a la derecha por ejemplo:
InputSimulator.SimulateModifiedKeyStroke(VirtualKeyCode.LWIN,VirtualKeyCode.RIGHT);
Y luego hay otra solución que proponen en el siguiente enlace sin necesidad de utilizar proyectos externos:
http://stackoverflow.com/questions/6407584/sendkeys-send-and-windows-key
He realizado algunas pruebas con una aplicación de consola y funciona bien. Lo que no se es como se podría combinar el uso del combo WINKEY+(LOQUESEA) para por ejemplo dejar 3 ventanas apiladas verticalmente, como en tu ejemplo de notepad.
Espero sirva algo de ayuda esto.
Un saludo!