sábado 25 de octubre de 2008

[CSharp]Fun with Windows Handlers (3/3)

Y por fin la tercera y última entrada, donde -por fin- utilizaremos todo lo que hemos aprendido anteriormente para realmente hacer algo útil :)

Ya vimos anteriormente los conceptos de parent y owner de un control, ahora vamos a ver cómo podemos forzar esos comportamientos en una aplicación por medio de los Handlers de la ventana y de llamadas a la API Win32.

Al final de la entrada se puede descargar el proyecto de visual studio 2005 utilizado para las capturas de pantalla de este post. La aplicación crea tres formularios simples: un reloj con un par de botones que simplemente muestran un aviso al ser pulsados, un formulario para guardar una imagen, y un formulario con diferentes botones de control para activar y desactivar las operaciones entre los anteriores formularios.

Formularios creados por la aplicación de ejemplo 

Botón 'Make Owner'

Al pulsar este botón provocará que el formulario 'ClockWindow' se convierta en el Owner del formulario 'PictureBox'. Por tanto éste último siempre será dibujado por encima del primero:

Nótese que la ventana activa es 'ClockWindow' y aún así 'PictureWindow' permanece por encima 

Para conseguir este efecto, usaremos la función SetWindowLongPtr(), que sirve para cambiar los atributos de una ventana. Si firma en P/Invoke es:

public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong)

Donde hWnd es el handle de la ventana a la que queremos cambiar un atributo, nIndex es una constante que define el atributo a cambiar, y dwNewLong sirve para establecer el nuevo valor.


En este caso para cambiar el owner de la ventana, tenemos que usar una constante con un desafortunado nombre: GWL_HWNDPARENT. Ésta constante está definida en winapi.h, y tiene el valor -8. Su nombre es desafortunado porque realmente no tiene nada que ver con establecer la clase padre (parent) y puede llevar a equívocos.


Para establecer, por ejemplo, que el formulario 'ClockWindow' sea el propietario de 'PictureBox' llamaremos a la función de la siguiente manera:

WinAPIDeclarations.SetWindowLongPtr(
pictureWindowForm.Handle,
-8,
clockWindowForm.Handle);

Y para volver a dejar todo como estaba, lo que haremos será establecer el Handler del owner de 'PictureBox' como IntPtr.Zero, lo que equivale a establecer el escritorio como propietario:

WinAPIDeclarations.SetWindowLongPtr(
pictureWindowForm.Handle,
-8,
IntPtr.Zero); 

Botón 'Make Parent' y 'Make Parent to Button'


Seguimos con la función SetParent(), cuya firma para P/Invoke se define:

[DllImport("user32.dll")]
public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

Con esta función podemos forzar que un control cualquiera se convierta en el padre del otro, y por tanto provocará que el hijo se dibuje dentro del rectángulo de cliente del control padre, ya sea un formulario u otro control:


PictureWindow es ahora hija de ClockWindow  PictureWindow es ahora hija de un botón en ClockWindow 


Hay que destacar que tanto esta operación como la anterior funcionan para cualquier ventana de la que podamos obtener el handle. Por tanto podemos conseguir cosas como esta:


Windows Live Writter es ahora el padre de PictureWindow. No es Potosof :) 


Botón 'Disable refresh'


Esta operación permite activar o desactivar el repintado de una ventana. Utilizaremos la función SendMessage() para ello y el mensaje WM_SETREDRAW. La firma P/Invoke de la función y el valor del mensaje es:

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
public static readonly uint WM_SETREDRAW = 0x000B; 

Para desactivar el repintado de una ventana haremos:


IntPtr result = WinAPIDeclarations.SendMessage(
clockWindowForm.Handle,
WinAPIDeclarations.WM_SETREDRAW,
(IntPtr)0,
IntPtr.Zero);
Y para volver a activarlo:
IntPtr result = WinAPIDeclarations.SendMessage(
clockWindowForm.Handle,
WinAPIDeclarations.WM_SETREDRAW,
(IntPtr)1,
IntPtr.Zero);

Botón 'Disable Input'


Esta operación permite activar o desactivar el procesado de input para una ventana, es decir, podemos hacer que la ventana no responda a ningún tipo de entrada de datos (con ratón o teclado) de forma que en la práctica no se puede interactuar con ella. Para ello disponemos de otra función con un nombre no muy descriptivo: EnableWindow(), que tiene la firma:

[DllImport("user32.dll")]
public static extern bool EnableWindow(IntPtr hWnd, bool bEnable);
Viendo los argumentos es bastante fácil de usar si eres capaz de intuir por el nombre lo que hace; para desactivar la entrada de input haremos:
EnableWindow(clockWindowForm.Handle, false);
y para desactivarla:
EnableWindow(clockWindowForm.Handle, true);

De nuevo, estas dos últimas operaciones pueden aplicarse a cualquier ventana sabiendo su handle, así que podemos desactivar el repintado o la entrada de datos de una ventana externa. Esto puede ser útil en ciertos casos pero hay que tener mucho cuidado: si olvidamos restablecer el estado anterior o nuestra aplicación se cierra inesperadamente podríamos dejar una aplicación externa sin respuesta, por lo que el usuario tendría que matar el proceso y recargarla, potencialmente perdiendo datos. Hay que ser muy cuidadoso al utilizar estas operaciones.


También hay que avisar a posibles usuarios de Mono de que al estar haciendo llamadas específicas a la API de Windows, este código no es portable.


Descargar ejemplo para VisualStudio 2005

sábado 18 de octubre de 2008

[CSharp]Fun with Windows Handlers (2/3)

Siguiendo con la entrada anterior, veremos ahora una pequeña muestra de alguna cosa divertida conociendo el Handler de una ventana.

Si la ventana pertenece a nuestra aplicación, y sólo en ese caso, podemos obtener a partir del handle, la instancia de la clase Form al que está asociado:

Form formApp = (Form)Form.FromHandle( myAppWindowHandle );
Si el handle no pertenece a una ventana creada dentro de nuestra propia aplicación, Form.FromHandle retorna null, imagino que será por cuestiones de seguridad. El caso es que usando funciones de Win32 API por medio de P/Invoke podemos saltarnos en algunos casos esta seguridad.



Antes de mostrar lo que podemos hacer, tendremos que presentar dos conceptos que se aplican en las ventanas de Windows. Si, es un momento teórico, pero necesario.



Parent Window



En Windows, todo elemento visual (widgets), desde el formulario principal a los botones se consideran... windows (buen nombre para no confundir a la gente ¿eh? ) Una mejor manera de denominar a los widgets es usar la nomenclatura usada en .NET: Todo es un Control.



Una aplicación de Windows consiste en una jerarquía de controles: cada control puede a su vez contener a otros, ser padre de otros, creando una jerarquía en forma árbol. En Visual Studio podemos ver esa jerarquía por medio del menú View/Other Windows/Outline Document.



Por ejemplo, si tuviéramos el siguiente formulario la jerarquía sería la mostrada por la imagen a su derecha:



parentWindow1 parentWindowTree1



Sin embargo si ahora añadimos un GroupBox e introducimos el botón en él, tendríamos la siguiente ventana y jerarquía:



parentWindow2 parentWindowTree2



Una vez dicho esto, creo que queda claro de forma intuitiva lo que es una ventana padre o una ventana hija dentro de la jerarquía. Lo que tenemos que tener en cuenta que una ventana hija se posiciona en coordenadas relativas a la ventana padre, y que siempre se mantiene dentro de los límites del área de cliente de una ventana.



WindowParts





Owner Window



Esta es más sencilla: una ventana se considera propietaria de otra cuando se considera parte fundamental de la ventana propietaria. Pro ejemplo, si desde nuestra aplicación creamos un MessageBox, o mostramos un diálogo de selección de fichero, nuestra aplicación es la propietaria de éstas ventanas: nuestra aplicación las crea y las gestiona, pero no dependen de una jerarquía de controles dentro de la ventana principal. Esas ventanas no tienen por qué se modales, por ejemplo, las típicas ventanas flotantes de una aplicación de retoque fotográfico tienen como propietaria a la ventana de la aplicación principal:



La ventana principal es propietaria de las sombreadas en rojo



Las ventanas que son propiedad de otra tienen la particularidad de que se pueden dibujar en cualquier parte del escritorio, pero siempre se muestran por encima de la ventana propietaria.


UPDATE: Otro comportamiento muy importante de una ventana que tiene como propietaria a otra es que si cerramos la ventana principal, todas las ventanas de las que la ventana principal sea propietaria también se ocultarán.


 


Y por fin, veremos cómo hacer alguna cosa interesante con un handler en la tercera y última entrada de esta mini-serie.

viernes 17 de octubre de 2008

[CSharp]Fun with Windows Handlers (1/3)

Hay una cosa segura: .Net tiene sus limitaciones. Al menos, si lo comparamos con las operaciones que teníamos disponibles con la API Win32, veremos que no podemos hacer absolutamente todo lo que hacíamos con ésta última. Para solucionar este eventual problema tenemos Platform Invocation Services, más conocido como PInvoke (o P/Invoke), que nos permite hacer llamadas a código nativo implementadas en una DLL desde código administrado.

Debido a que las estructuras de datos utilizadas en código nativo son distintas a las de código administrado, debemos realizar un proceso conocido como Marshaling, que grosso modo viene a significar transportar datos entre diferentes contextos. Muy genérico ¿verdad?.

Este proceso de adecuación de los datos es prácticamente automático. Lo único que tenemos que hacer es indicarle al framework .net la función de la Dll que queremos utilizar, y decirle cómo debe traducir las estructuras de datos. Y ni siquiera tenemos que perder el tiempo en ello, ya que gracias a la página al wiki PInvoke.net, tenemos acceso a un motor de búsqueda donde sólo debemos introducir el nombre de la función que buscamos y obtendremos el código necesario para usarla, listo para cortar y pegar. Aún así daremos una pequeña explicación:

Para usar una función que exporta una Dll tenemos que seguir los siguientes pasos:

Crear una función estática y con el keyword extern.

Marcamos la función con el atributo DllImport, al cual pasamos como parámetro el nombre de la Dll que contiene al método.

La función por supuesto debe tener los parámetros adecuados a lo tipos de .NET.

Por ejemplo, la función Win32 API FindWindow tiene esta definición:

HWND FindWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName );
Esta función recibe un puntero a una cadena con la nombre de la clase con la que se registró la ventana (ojo esto no tiene que ver con clases de POO, sino con otro tipo de clase), y otro puntero a una cadena con el nombre de la ventana (lo que sería el título o caption), retornando el HANDLE de la ventana si la encuentra. Bien, esto se traduce en .NET con el siguiente método de clase (en .NET no existen las funciones por separado, todo tiene que ser parte de un objeto)
public static class Win32API{    [DllImport("user32.dll")]public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);}



Vemos que el Handler de la ventana se traduce por una estructura System.IntPtr, y los punteros a cadenas por strings. Ahora podremos llamar a la función, por ejemplo:

IntPtr hwn = FindWindow(null, "título de la ventana");



Y buscaría la ventana con dicho título ignorando su clase de ventana, retornando el handler a la ventana (o IntPtr.Zero si no se encuentra una ventana con ese título)




Teniendo el Handler de una ventana podemos controlar prácticamente todo si sabemos lo que hacemos. Y esta función nos permite buscar y obtener el handler de cualquier ventana. Aunque no sea de nuestra aplicación. Aunque ni siquiera haya sido generada por código .NET...




Veremos algunos conceptos más en la siguiente entrada.




PS: Como nota curiosa, podemos llamar a nuestro método estático como queramos, siempre que en el atributo [DllImport] le indiquemos el nombre real de la función de la Dll que pretendemos importar:


[DllImport("user32.dll", EntryPoint="FindWindow")]
public static extern IntPtr BuscaHandlerDeVentana(string lpClassName, string lpWindowName);



Como vemos si el parámetro EntryPoint no está disponible, se toma como el mismo el nombre del método estático.




PPS: Existen otras maneras de obtener un handle de la ventana. Si tenemos un objeto System.Windows.Forms.Control (lo que incluye un System.Windows.Forms.Form), éste expone la propiedad Handle, que nos devuelve el Handler de la ventana.




Del mismo modo la clase System.Diagnostics.Process, expone el método estático GetProcesses(), que nos devuelve una lista con todos los Procesos en ejecución (podemos buscar por PID, nombre, etc). A su vez una instancia de la clase System.Diagnostics.Process expone el método Handle, con el Handler del proceso (que suele coincidir con el de la ventana principal).


UPDATE: La clase Process expone una propiedad denominada MainWindowHandle, que SI devuelve el handle de la ventana principal del proceso :)

miércoles 1 de octubre de 2008

Links de la semana

Algo retrasados, pero aquí están los links de la "semana"agrupados un poco...

Test Driven Development

  • Google nos ofrece Moq, una librería de Mocks, simple de utilizar y aprovechando las características del framework .NET 3.5. La verdad es que sin haberla probado pero viendo los pequeños snippets de código de la web tiene una pinta muy interesante. Yo he estado usando Rhino Mocks, y aunque es muy potente, es un lío que te cagas algo compleja al principio.
  • Y más Mocks, pero esta vez para J2ME. MockME es un mock completo de las librerías que conforman la especificación J2ME. De esta forma podemos escribir casos de prueba para nuestra aplicación J2ME, pero sin necesidad de ejecutarlos en el móvil.

Visual Studio


Miscelánea:

  • Si eres el poseedor de una PlayStation Eye, y además usas Windows, aquí tienes a un campeón que se ha currado un driver para la misma.
  • Blog oficial de google sobre Mac, con novedades y utilidades centradas en lo relacionado con Apple.
  • Booklet Creator es una página web a la que le puedes subir cualquier PDF y te lo transforma para que puedas imprimirlo a dos caras como un libro. Muy útil si el driver de tu impresora no te permite hacerlo de serie, o quieres llevar a imprimir el PDF directamente a una copistería.
  • OpenPandora. Una consola open source, al estilo de la GP2X o su antecesora la GP32, que por fin está a punto de ser lanzada. Destacan sus especificaciones técnicas:

* ARM® Cortex™-A8 600Mhz+ CPU running Linux
* 430-MHz TMS320C64x+™ DSP Core
* PowerVR SGX OpenGL 2.0 ES compliant 3D hardware
* 800x480 4.3" 16.7 million colours touchscreen LCD
* Wifi 802.11b/g, Bluetooth & High Speed USB 2.0 Host
* Dual SDHC card slots & SVideo TV output
* Dual Analogue and Digital gaming controls
* 43 button QWERTY and numeric keypad
* Around 10+ Hours battery life

Vale, lo de la batería no me lo trago hasta que no lo vea, y aunque guapa lo que se dice guapa, la consola no es, tampoco es un adefesio. Pero sus specs son impresionantes, y el hecho de que sea abierta es su principal virtud. Su precio es de 250€, que tal y como está el mercado es bastante competitivo. Eso si, si estás interesado más te vale reservar ya porque sólo van a lanzar 3000 en lo que queda de año.


Y la próxima "semana", más ;)