Renderizar detrás de los iconos del escritorio en Windows

Dibuja o renderiza un formulario de Windows directamente sobre el fondo de pantalla, detrás de los íconos del escritorio en Windows.

Introducción

Quienes lean este artículo probablemente conozcan DreamScene, la función de Windows Vista que permite renderizar secuencias de vídeo (en formato .dream) como fondo de escritorio. También existe una herramienta llamada Rainmeter que permite colocar widgets, gadgets o cualquier otro elemento en el escritorio, uno encima del otro, arriba y abajo. También existe Winamp, el primer programa donde vi el renderizado de DirectX en acción detrás de los iconos del escritorio.

Estas herramientas tienen una cosa en común: no pueden hacerlo en Windows 8, al menos la parte de “dibujar debajo de los íconos del escritorio”.

Cómo funcionaba antes

Existe el árbol de ventanas. Este árbol contiene todas las ventanas que se muestran u ocultan en el escritorio actual. Además, existe una herramienta llamada Spy++ (Visual Studio -> Herramientas -> Spy++), que permite visualizar y navegar por dicho árbol. Esta herramienta forma parte de Visual Studio.

El Código

Obtener la dirección del administrador del programa

Primero, buscamos el identificador de la ventana Progman. Podemos usar la función FindWindow proporcionada por la API de Windows para realizar esta tarea.

IntPtr progman = W32.FindWindow("Progman", null);

Enviar mensaje al administrador del programa

Para activar la creación de una ventana WorkerW entre los iconos del escritorio y el fondo de pantalla, debemos enviar un mensaje al Administrador de programas. Este mensaje no está documentado, por lo que no tiene un nombre especial en la API de Windows, excepto 0x052C. Para enviarlo, usamos el método . de la API de Windows SendMessageTimeout.

IntPtr result = IntPtr.Zero;

W32.SendMessageTimeout(progman, 
                       0x052C, 
                       new IntPtr(0), 
                       IntPtr.Zero, 
                       W32.SendMessageTimeoutFlags.SMTO_NORMAL, 
                       1000, 
                       out result);

Obtener el control de la ventana recién creada

Ahora, necesitamos obtener un identificador para la ventana WorkerW recién creada. Dado que hay más de una ventana con el título "" y la clase " WorkerW", debemos recorrer el árbol de ventanas secuencialmente. Esto se puede hacer usando la función EnumWindows.

EnumWindows realiza llamadas EnumWindowProc para cada ventana de nivel superior. Desde ahí, podemos comprobar si la ventana actual contiene un elemento secundario llamado “SHELLDLL_DefView”, lo que indica que representa los iconos del escritorio. A continuación, tomamos el siguiente elemento secundario de esa ventana.

// Spy++ output
// .....
// 0x00010190 "" WorkerW
//   ...
//   0x000100EE "" SHELLDLL_DefView
//     0x000100F0 "FolderView" SysListView32
// 0x00100B8A "" WorkerW       <-- Esta es la instancia
// 0x000100EC "Program Manager" Progman

IntPtr workerw = IntPtr.Zero;

W32.EnumWindows(new W32.EnumWindowsProc((tophandle, topparamhandle) =>
{
    IntPtr p = W32.FindWindowEx(tophandle, 
                                IntPtr.Zero, 
                                "SHELLDLL_DefView", 
                                IntPtr.Zero);

    if (p != IntPtr.Zero)
    {
        // Gets the WorkerW Window after the current one.
        workerw = W32.FindWindowEx(IntPtr.Zero, 
                                   tophandle, 
                                   "WorkerW", 
                                   IntPtr.Zero);
    }

    return true;
}), IntPtr.Zero);

Prueba 1: Dibujar gráficos entre iconos y fondos de pantalla

Con el workerw obtenido, empieza la diversión. La primera prueba trata sobre cómo usar las clases System.Drawing para dibujar algo.

Esta prueba dibuja un rectángulo en la esquina superior izquierda del escritorio. Si usas varios monitores, ten en cuenta que el área del escritorio ocupa un rectángulo en todos ellos. Por lo tanto, asegúrate de que el monitor izquierdo esté encendido y que la ubicación del monitor asigne la esquina superior izquierda a un monitor, en caso de tener cuatro, uno encima de los otros tres.

NOTA
Todo lo que dibujes en esta capa permanecerá allí hasta que pintes sobre ella, la invalides o restablezcas tu fondo de pantalla.
IntPtr dc = W32.GetDCEx(workerw, IntPtr.Zero, (W32.DeviceContextValues)0x403);
if (dc != IntPtr.Zero)
{
    using (Graphics g = Graphics.FromHdc(dc))
    {
        g.FillRectangle(new SolidBrush(Color.White), 0, 0, 500, 500);

    }
    W32.ReleaseDC(workerw, dc);
}

Prueba 2: Colocar un formulario de Windows detrás de los iconos del escritorio

Esta prueba muestra cómo colocar un formulario Windows Form normal detrás de los iconos del escritorio. En esencia, esto se puede lograr asignando a nuestra ventana WorkerW el elemento principal de un formulario Windows Form. Para ello, podemos usar la función SetParent de la API de Windows.

NOTA
Para que esta función funcione, el formulario debe estar ya creado. El evento form.Load parece ser el lugar adecuado.
Form form = new Form();
form.Text = "Prueba Window";

form.Load += new EventHandler((s, e) =>
{
    form.Width = 500;
    form.Height = 500;
    form.Left = 500;
    form.Top = 0;

    Button button = new Button() { Text = "Catch Me" };
    form.Controls.Add(button);
    Random rnd = new Random();
    System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
    timer.Interval = 100;
    timer.Tick += new EventHandler((sender, eventArgs) =>
    {
        button.Left = rnd.Next(0, form.Width - button.Width);
        button.Top = rnd.Next(0, form.Height - button.Height);
    });
    timer.Start();
    W32.SetParent(form.Handle, workerw);
});

Application.Run(form);

Conclusión

Un solo mandato para gobernarlos a todos 😄

W32.SendMessageTimeout(W32.FindWindow("Progman", null), 
                       0x052C, 
                       new IntPtr(0), 
                       IntPtr.Zero, 
                       W32.SendMessageTimeoutFlags.SMTO_NORMAL, 
                       1000, 
                       out result);

/img/ref.png
.