Desktop Window Manager, los entresijos de la interfaz visual de Windows Vista (2ª Parte)

En la primera parte del presente artículo introducimos los conceptos básicos de la composición de escritorio y las particularidades de esta tecnología aplicadas a Windows Vista. También vimos cómo empezar a trabajar con esta tecnología desde .NET. En esta segunda entrega trataremos de implementar un pequeño ejemplo algo más completo. El objetivo que vamos a tratar de encarar no es más que una simple aplicación que emule el Flip de Windows Vista. Manos a la obra; El Flipador Pues bien, del dicho al hecho. Vamos a tratar de hacer nuestro propio conmutador de ventanas, al cual me he atrevido a bautizar como El Flipador J. El trabajo se puede desglosar mediante los siguientes pasos:


  1. Obtener todas las ventanas abiertas por el sistema operativo.

  2. Crear una ventana para mostrar las miniaturas, a la que redimensionaremos en función del número de ventanas obtenidas en el paso anterior.

  3. Dibujar las miniaturas en la ventana.

  4. Aplicar el efecto glass.
NOTA: Puede resultarnos útil, de cara a simplificar la programación, conocer una de las limitaciones que Windows Vista asigna a la gestión de ventanas en modo de composición de escritorio, y es que “únicamente” podremos mantener abiertas de forma simultánea 52 ventanas. ¿Suficiente? Espero que sí… Para recuperar todas las ventanas abiertas recurriremos al API de Windows user32.dll, y más concretamente la función EnumWindows, a la que indicaremos un delegado que será invocado para cada una de las ventanas, mediante el cual determinaremos que pantallas deben procesarse y dónde crearemos la lista de ventanas definitiva que conformarán el conjunto de miniaturas. Para cada iteración de EnumWindows se proporciona un puntero a una ventana, que usaremos en conjunto con la función GetWindowLongA, también incluida en user32.dll, para obtener todos los datos del formulario, mediante los cuales podemos determinar si la ventana tiene bordes y título, en cuyo caso la añadiremos a nuestra lista. Como último recurso de la librería user32.dll también usaremos la función GetWindowText, con la que podremos obtener el título de la ventana. Para confeccionar la lista, usaremos una pequeña clase con los datos que necesitemos de los obtenidos de GetWindowLongA y GetWindowText. De este modo podremos dar por finalizado el primer paso de nuestro proyecto (Código 4). Definición de constantes (Código 4a)
1 public static readonly int GWL_STYLE = 16;
2 public static readonly ulong WS_VISIBLE = 0x10000000L;
3 public static readonly ulong WS_BORDER = 0x00800000L;
4 public static readonly ulong TARGETWINDOW = WS_BORDER | WS_VISIBLE;
Importar declaraciones (Código 4b)
 1 //Gestión de ventanas Win32
2 [DllImport(«user32.dll«)]
3 public static extern int EnumWindows(EnumWindowsCallback
4 funcionCallBack, int parametrosFuncion);
5 public delegate bool EnumWindowsCallback(IntPtr manejador, int
6 parametros);
7 [DllImport(«user32.dll«)]
8 public static extern ulong GetWindowLongA(IntPtr manejador, int
9 indice);
10 [DllImport(«user32.dll«)]
11 public static extern void GetWindowText(IntPtr hWnd, StringBuilder
12 lpString, int nMaxCount);
Clase Ventana (Código 4c)
1 class Ventana
2 {
3 public string Titulo;
4 public IntPtr Manejador;
5 }
Implementación (Código 4d)
 1 List<Ventana> ventanas;
2
3 private bool Callback(IntPtr manejador, int parametros)
4 {
5 //Comprobar que no se trate de la propia ventana
6 //y recoger la información de la ventana en caso de
7 //tener bordes y título
8 if (this.Handle != manejador && (GetWindowLongA(manejador, GWL_STYLE) & TARGETWINDOW) == TARGETWINDOW)
9 {
10 StringBuilder tituloVentana = new StringBuilder(100);
11 GetWindowText(manejador, tituloVentana, tituloVentana.Capacity);
12 Ventana ventana = new Ventana();
13 ventana.Manejador = manejador;
14 ventana.Titulo = tituloVentana.ToString();
15 ventanas.Add(ventana);
16 }
17 return true;
18 }
19 //Obtener lista de ventanas
20 ventanas = new List<Ventana>();
21 EnumWindows(Callback, 0);
Ahora nos toca determinar que espacio debe ubicar cada una de las miniaturas, y que tamaño debe tener la pantalla contenedora. Para ello iteraremos la lista de ventanas que ya tenemos confeccionada, y para cada una de ellas crearemos una miniatura y calcularemos la posición de la misma. Usaremos también la ubicación calculada para dibujar un panel de forma paralela a cada una de las miniaturas, lo que nos permitirá simplificar la tarea de capturar eventos relacionados con las miniaturas, ya que pintaremos los nuevos paneles con negro, consiguiendo así que una vez que apliquemos el efecto glass los paneles no se muestren, pudiendo emular el efecto de que el panel responde a los eventos de la miniatura correspondiente (Código 5). Definición de constantes (Código 5a)
1 public static readonly int DESPLAZAMIENTO_VERTICAL = 40;
2 public static readonly int ANCHO_MINIATURA = 160;
3 public static readonly int ALTO_MINIATURA = 120;
4 public static readonly int ESPACIADO_HORIZONTAL = 10;
5 public static readonly int ESPACIADO_VERTICAL = 10;
6 public static readonly int MAXNUM_MINIATURAS_HORIZONTAL = 4;
Implementación (Código 5b)
 1 Form flipador = new Form();
2 //Iniciar proceso de cálculo del tamaño de la ventana
3 int anchoVentana = 10 + ESPACIADO_HORIZONTAL;
4 int altoVentana = DESPLAZAMIENTO_VERTICAL + 30 + ESPACIADO_VERTICAL +
5 ALTO_MINIATURA + ESPACIADO_VERTICAL;
6 int fila = 1; int columna = 1;
7 //Recorrer cada una de las ventanas
8 foreach (Ventana ventana in ventanas)
9 {
10 //Crear panel receptor de eventos
11 Panel p = new Panel();
12 p.Tag = ventana;
13 IntPtr miniatura;
14 p.BackColor = Color.Black;
15 p.Parent = flipador;
16 p.Top = DESPLAZAMIENTO_VERTICAL + ESPACIADO_VERTICAL + (fila 1) * (ALTO_MINIATURA + ESPACIADO_VERTICAL);
17 p.Left = ESPACIADO_HORIZONTAL + (columna 1) * (ANCHO_MINIATURA + ESPACIADO_HORIZONTAL);
18 p.Height = ALTO_MINIATURA; p.Width = ANCHO_MINIATURA;
19 //Capturar eventos del panel
20 p.MouseMove += new
21 System.Windows.Forms.MouseEventHandler(panel_MouseMove);
22 p.MouseClick += new
23 System.Windows.Forms.MouseEventHandler(panel_Click);
24 //Registrar miniatura
25 miniatura = DwmRegisterThumbnail(flipador.Handle, (IntPtr)(ventana.Manejador));
26 ActualizarMiniatura(miniatura, p.Left, p.Top, p.Left + ANCHO_MINIATURA, p.Top + ALTO_MINIATURA);
27 //Recalcular tamaño ventana
28 if (fila == 1) anchoVentana = anchoVentana + ESPACIADO_HORIZONTAL + ANCHO_MINIATURA;
29 if (columna == MAXNUM_MINIATURAS_HORIZONTAL)
30 {
31 fila += 1; columna = 0;
32 altoVentana = altoVentana + ESPACIADO_VERTICAL + ALTO_MINIATURA;
33 }
34 columna += 1;
35 }
36 if (columna == 1) altoVentana = altoVentana ESPACIADO_VERTICAL ALTO_MINIATURA;
37 flipador.Height = altoVentana;
38 flipador.Width = anchoVentana;
39 flipador.Refresh();
40 flipador.Show();
Ya que hemos aprovechado los paneles para capturar eventos, usaremos MouseMove mostrar el título de la ventana representada por la miniatura que el cursor del ratón sobre vuele, y el evento MouseClick para maximizar dicha ventana. Mostrar la ventana seleccionada Implementaremos el evento MouseClick con la ayuda de la función SetWindowPos de la librería user32.dll. Invocaremos una primera vez la función para activar la pantalla, y una segunda, para maximizarla por encima de la ventana de selección (Código 6). Definición de constantes (Código 6a)
1 public static readonly int SWP_SHOWWINDOW = 3;
Importar declaraciones (Código 6b)
1 [DllImport(«user32.DLL«)]
2 private extern static void SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, int wFlags);
Implementación (Código 6c)
1 private void panel_Click(object sender, EventArgs e)
2 {
3 Panel p;
4 Ventana v;
5 p = (Panel)sender;
6 v = (Ventana)p.Tag;
7 ShowWindow(v.Manejador, 0);
8 ShowWindow(v.Manejador, SWP_SHOWWINDOW);
9 }
Mostrar títulos de ventana De cara a mostrar el título de la miniatura la primera opción será usar un control label, pero este camino acabaremos por desestimarlo debido a problemas como la máscara de trasparencia (dificultades para mostrar texto en negro), o por el renderizado que afecta a la calidad de los gráficos, produciendo contornos entrecortados en el perfil de los textos si queremos mostrarlo sobre la superficie semitransparente. Dado que al escribir sobre una superficie glass estamos en un entorno totalmente gráfico, hemos de usar herramientas capaces de suavizar el contorno de nuestros textos para mostrar resultados más profesionales. La buena noticia es que por fin entramos en territorio administrado, es decir, dejamos de un lado las API’s para trabajar con librerías nativas de .NET, y en este caso concreto System.Drawing, mediante la cual usaremos la tecnología GDI+ para llevar a cabo nuestros propósitos. Ahora podremos escribir textos en color negro sin problemas con las transparencias (Código 7).
 1 Graphics g = flipador.CreateGraphics();
2
3 private void panel_MouseMove(object sender, MouseEventArgs e)
4 {
5 Panel p;
6 Ventana v;
7 p = (Panel)sender;
8 v = (Ventana)p.Tag;
9 g.Clear(Color.Black);
10 GraphicsPath lienzo = new GraphicsPath();
11 SolidBrush brocha = new SolidBrush(Color.Black);
12 lienzo.AddString(v.Titulo, new FontFamily(«Arial«), (int)FontStyle.Bold, 20, new Point(10, 10), StringFormat.GenericDefault);
13 g.SmoothingMode = System.Drawing.Drawing2D.
14 SmoothingMode.HighQuality;
15 g.FillPath(brocha, lienzo);
16 }

Puliendo el resultado Nuestro Flipador v1.0 ya está terminado (Imagen 5), y como podemos observar tiene un aspecto francamente atractivo. Le hemos aplicado el efecto glass tal y como mostrábamos en el segundo fragmento de código, y podemos sentirnos satisfechos por el trabajo, a no ser que seamos un “pelín” quisquillosos. Vamos a fijarnos en el siguiente detalle: Si examinamos con atención la imagen 5 y comparamos el texto del título de la ventana de la aplicación, con el texto del título de la miniatura, veremos que a parte de la obvia diferencia de tamaños, el título del formulario muestra un atractivo detalle en forma de sombreado claro, que facilita la lectura al contrastar con el color de la fuente. Este detalle se escapa del ámbito de competencias de DWM, siendo una característica concreta del tema visual Aero Glass, con lo que si queremos conseguir los mismos resultados en nuestra aplicación deberemos recurrir a la librerías que usa Windows para gestionar los temas visuales del escritorio, UxTheme.dll y Gdi32.dll. La técnica a grandes rasgos consiste en capturar el espacio de memoria donde se mostrará el texto, dibujarlo, y solicitar al gestor de estilo que lo renderice según unos parámetros determinados; en este caso, el efecto “glaseado” bajo el texto (Código 8).


Imagen 5


image


Definición de constantes y estructuras (Código 8a)

 1 private const int DTT_COMPOSITED = 8192;
2 private const int DTT_GLOWSIZE = 2048;
3 private const int DTT_TEXTCOLOR = 1;
4
5 [StructLayout(LayoutKind.Sequential)]
6 private class BITMAPINFO {
7 public int biSize; public int biWidth; public int biHeight;
8 public short biPlanes; public short biBitCount;
9 public int biCompression; public int biSizeImage;
10 public int biXPelsPerMeter; public int biYPelsPerMeter;
11 public int biClrUsed; public int biClrImportant;
12 public byte bmiColors_rgbBlue; public byte bmiColors_rgbGreen; public byte bmiColors_rgbRed;
13 public byte bmiColors_rgbReserved;}
14
15 [StructLayout(LayoutKind.Sequential)]
16 private struct RECT {
17 public int Left; public int Top; public int Right; public int Bottom;
18 public RECT(int left, int top, int right, int bottom) {
19 Left = left; Top = top; Right = right; Bottom = bottom; }
20 public RECT(Rectangle rectangle) {
21 Left = rectangle.X; Top = rectangle.Y; Right = rectangle.Right; Bottom = rectangle.Bottom; }
22 public Rectangle ToRectangle() {
23 return new Rectangle(Left, Top, Right Left, Bottom Top); }
24 public override string ToString() {
25 return «Left: « + Left + «, « + «Top: « + Top + «, Right: « + Right + «, Bottom: « + Bottom; } }
26
27 [StructLayout(LayoutKind.Sequential)]
28 private struct POINT {
29 public POINT(int x, int y) { this.x = x; this.y = y; }
30 public int x; public int y; }
31
32 StructLayout(LayoutKind.Sequential)]
33 private struct DTTOPTS {
34 public int dwSize; public int dwFlags;
35 public int crText; public int crBorder;
36 public int crShadow; public int iTextShadowType;
37 public POINT ptShadowOffset;
38 public int iBorderSize; public int iFontPropId;
39 public int iColorPropId; public int iStateId;
40 public bool fApplyOverlay;
41 public int iGlowSize; public int pfnDrawTextCallback;
42 public IntPtr lParam; }
43
Importar declaraciones (Código 8b)
 1 [DllImport(«gdi32.dll«, ExactSpelling = true, SetLastError = true)]
2 private static extern IntPtr CreateCompatibleDC(IntPtr hDC);
3 [DllImport(«gdi32.dll«, ExactSpelling = true)]
4 private static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
5 [DllImport(«gdi32.dll«, ExactSpelling = true, SetLastError = true)]
6 private static extern bool DeleteObject(IntPtr hObject);
7 [DllImport(«gdi32.dll«, ExactSpelling = true, SetLastError = true)]
8 private static extern bool DeleteDC(IntPtr hdc);
9 [DllImport(«gdi32.dll«)]
10 private static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr
11 hdcSrc, int nXSrc, int nYSrc, uint dwRop);
12 [DllImport(«UxTheme.dll«, CharSet = CharSet.Unicode)]
13 private static extern int DrawThemeTextEx(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, string text, int iCharCount, int dwFlags, ref RECT pRect, ref DTTOPTS pOptions);
14 [DllImport(«gdi32.dll«)]
15 private static extern IntPtr CreateDIBSection(IntPtr hdc, BITMAPINFO pbmi, uint iUsage, int ppvBits, IntPtr hSection, uint dwOffset);
Implementación (Código 8c)
 1 private void panel_MouseMove(object sender, MouseEventArgs e)
2 {
3 Panel p;
4 Ventana v;
5 p = (Panel)sender;
6 v = (Ventana)p.Tag;
7 //Identificar el dispositivo de video
8 IntPtr salida = g.GetHdc();
9 //Crear un contexto del dispositivo de video
10 IntPtr contextoSalida = CreateCompatibleDC(salida);
11 //Crear estructura de imagen
12 BITMAPINFO estructuraImagen = new BITMAPINFO();
13 estructuraImagen.biSize = Marshal.SizeOf(estructuraImagen);
14 estructuraImagen.biWidth = margenes.Width;
15 estructuraImagen.biHeight = margenes.Height;
16 estructuraImagen.biPlanes = 1;
17 estructuraImagen.biBitCount = 32;
18 estructuraImagen.biCompression = 0;
19 //Crear imagen independiente
20 IntPtr imagen = CreateDIBSection(salida, estructuraImagen, 0, 0, IntPtr.Zero, 0);
21 SelectObject(contextoSalida, imagen);
22 // Establecer espacio y formato del texto
23 TextFormatFlags formatoTexto = TextFormatFlags.SingleLine | TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter;
24 Rectangle margenes = new Rectangle(0, 0, 800, 50);
25 IntPtr fuente = new Font(«Tahoma«, 20).ToHfont();
26 SelectObject(contextoSalida, fuente);
27 System.Windows.Forms.VisualStyles.VisualStyleRenderer renderer = new System.Windows.Forms.VisualStyles.VisualStyleRenderer (System.Windows.Forms.VisualStyles.VisualStyleElement.Window.Caption.Active);
28 //Capturar el texto mostrado, renderizarlo para aplicar el
29 //efecto glass, y el resultado almacebarlo en la imagen independiente
30 DTTOPTS opciones = new DTTOPTS();
31 opciones.dwSize = Marshal.SizeOf(typeof(DTTOPTS));
32 opciones.dwFlags = DTT_COMPOSITED | DTT_GLOWSIZE | DTT_TEXTCOLOR;
33 opciones.crText = ColorTranslator.ToWin32(Color.Black);
34 opciones.iGlowSize = 15; // This is about the size Microsoft Word 2007 uses
35 RECT margenesExtendidos = new RECT(0, 0, margenes.Right margenes.Left,
36 margenes.Bottom margenes.Top);
37 DrawThemeTextEx(renderer.Handle, contextoSalida, 0, 0, v.Titulo, 1, (int)formatoTexto, ref margenesExtendidos, ref opciones);
38 //Mostrar el resultado del renderizado
39 const int SRCCOPY = 0x00CC0020;
40 BitBlt(salida, margenes.Left, margenes.Top, margenes.Width, margenes.Height, contextoSalida, 0, 0, SRCCOPY);
41 //Liberar recursos
42 DeleteObject(fuente);
43 DeleteObject(imagen);
44 DeleteDC(contextoSalida);
45 g.ReleaseHdc(salida);
46 }

Ahora sí que ya tenemos todas las piezas de puzzle encajadas y podemos dar por refinados los más pequeños detalles (Imagen 6) de nuestra aplicación.


Imagen 6


image


Conclusiones Mediante una correcta gestión de las posibilidades de WDM podemos conseguir resultados profesionales para nuestras aplicaciones diseñadas para Windows Vista sin que ello repercuta en el rendimiento de nuestro código, obteniendo atractivos efectos para nuestras interfaces de usuario. Un placer para los ojos que los usuarios de nuestras soluciones sabrán agradecer.


[Crossposting desde http://www.tonirecio.com]

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *