Introduction
Those who read this article probably know DreamScene, that Windows Vista feature, that allows to render video sequences (in .dream format) as desktop background. There is also a tool called rainmeter, that allows you to place widgets/gadgets/whatever on your desktop in a top most, top and bottom position. Also there is winamp, the first program, where I saw directx rendering in action behind the desktop icons.
These tools have one thing in common, they cannot do it in Windows 8, at least the "draw under the desktop icons" part they don't.
EDIT:
The approach described in this article also works for Windows 10.
How It Used To Work (Windows XP, Vista, 7)
There is the window tree. This tree contains all windows that are currently displayed/or hidden on the current desktop and then there is a tool called Spy++ (Visual Studio -> Tools -> Spy++), that can be used to display and navigate that tree. That tool is part of Visual Studio.
The last leaf of this tree is the Program Manager. This window represents the whole shell. In Windows XP, Vista and 7 (Aero turned off) this Program Manager (Progman) contained a window (SysListView32
), that rendered the desktop icons. So if you set that Program Manager as your parent window, you could position yourself right behind those desktop icons. EDIT: Scroll to the comments section below, there is a comment from Kristjan Skutta, that describes how to do it in windows 7 with aero turned on: http://www.codeproject.com/Messages/5164145/Re-Windows.aspx
There is a great article on drawing to the desktop (in front of and behind desktop icons) here on CodeProject:
But for the life of me, I could not find an approach that worked for Windows 8.
The Problem with Windows 8+
Windows 7 and Windows 8 are very similar. To make the Windows XP approach work for Windows 7, you had to turn off aero desktop. With Windows 8, you cannot turn aero off, so there has to be another way.
The following image shows how the window tree is structured in Windows 8.
The SysListView32
is now separated from the Program Manager. This alone is not that big of a problem.
The problem is the desktop background. It is now fused with the Desktop Icons. So we can only draw over everything including the icons, or draw under everything including the background. It is not possible to position a window in the Z-Order so it is between the desktop icons and the desktop wallpaper. I tried every position.
A Way to Find a Solution
What led me to the solution presented in this article was the personalization dialog. When you set a wallpaper manually, you do not see a sudden change, instead the wallpaper is set using a smooth fade animation. In my opinion, such an animation is only possible if the system can somehow draw behind the desktop icons, since setting a wallpaper in rapid succession would be very slow and ugly.
So I set up Spy++, opened the personalization dialog and changed the wallpaper. It turns out, that when you change the desktop wallpaper, a new WorkerW
window between the WorkerW
instance that holds the Desktop Icons (SysListView32
) and the Desktop Manager is created.
I took the handle of that new WorkerW
window and put it into my test program and it was finally able to draw behind the desktop icons, directly above the wallpaper!
One problem remained. When I closed the personalization dialog, it took down that new WorkerW
window with it.
I had to find a way to trigger the creation of this WorkerW
window.
Spy++ reports the window as a sibling and child of the Program Manager, so it looks like the Program Manager creates it. I turned on message monitoring for the Program Manager using Spy++ and found what I was looking for.
Right after the click to change the desktop wallpaper, the Program Manager receives a bunch of messages. The first one being a user defined and undocumented message. This reeks after being "recently" added.
I extended the test program to send exactly this user defined message (0x052C) to the Program Manager. And it caused exactly what I was hoping for. After receiving the message, the Program Manager creates the WorkerW
window.
With all ingredients ready, I wrote a small demo application that illustrates how to draw on the desktop (behind the icons) and how to put a window behind the desktop icons.
The Code
Obtain Program Manager Handle
First, we begin by finding the handle of the Progman
window. We can use the FindWindow
function provided by the Windows API to accomplish this task.
IntPtr progman = W32.FindWindow("Progman", null);
Send Message to Program Manager
To trigger the creation of a WorkerW
window between the desktop icons and the wallpaper, we have to send the Program Manager a message. That message is an undocumented one, so there is no fancy Windows API name for it, except 0x052C
. In order to send the message, we use the Windows API method SendMessageTimeout
.
IntPtr result = IntPtr.Zero;
W32.SendMessageTimeout(progman,
0x052C,
new IntPtr(0),
IntPtr.Zero,
W32.SendMessageTimeoutFlags.SMTO_NORMAL,
1000,
out result);
Obtain Handle to Newly Created Window
Now, we have to obtain a handle to that newly created WorkerW
window. Since there is more than one window with title "" and class "WorkerW
", we have to go through the window tree sequentially. This can be done using the EnumWindows
function.
EnumWindows
calls supplied EnumWindowProc
for every top level window. From there, we can check if the current window contains a child named "SHELLDLL_DefView
", which indicates that the current window represents the desktop icons. We then take the next sibling of that window.
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)
{
workerw = W32.FindWindowEx(IntPtr.Zero,
tophandle,
"WorkerW",
IntPtr.Zero);
}
return true;
}), IntPtr.Zero);
Demo 1: Draw Graphics Between Icons and Wallpaper
With the workerw
handle in hand, the fun stuff begins. The first demo is about using the System.Drawing
classes to just draw something.
This demo draws a rectangle in the upper left corner of the desktop. If you use multiple monitors, be aware, that the desktop area spans a rectangle across all monitors, so make sure your left monitor is turned on and your monitor placement actually maps the top left corner to a monitor, in case you have four of them, with one atop the other three.
Note: Everything you draw onto this layer will stay there until you paint over it, invalidate it, or reset your wallpaper.
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);
}
Demo 2: Put a Windows Form behind desktop icons
This demo shows how to put a normal Windows Form behind desktop icons. In essence, this can be done by setting the parent of a Windows Form to our WorkerW
window. To set the parent of a form, we can use the SetParent
Windows API function.
Note: For this function to work, the form has to be already created. The form.Load
event seems to be the right place for it.
For the sake of a short example, I created the Form in place, without the "Project->Add Windows Form..." dialog and the designer.
Form form = new Form();
form.Text = "Test 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);
You will probably notice that there is no way to interact with the form, once its parent is set to the WorkerW
window. The desktop is not designed to have interactive children, so all events regarding mouse movement, keyboard input, etc. will not reach our Form.
There is a way around that. You can subscribe to the low level WH_KEYBOARD_LL
and WH_MOUSE_LL
events, also known from key loggers and mouse capture software. Via those events, you can receive mouse movements, clicks and key presses regardless of where they occur. You would have to forward those messages to your form and perform your own hit testing.
Conclusion
One command to rule them all
W32.SendMessageTimeout(W32.FindWindow("Progman", null),
0x052C,
new IntPtr(0),
IntPtr.Zero,
W32.SendMessageTimeoutFlags.SMTO_NORMAL,
1000,
out result);