在 Windows 8+ 的桌面图标后面绘制






4.97/5 (59投票s)
在 Windows 8+10 中直接在壁纸上,桌面图标下方绘制或渲染 Windows 窗体
引言
阅读本文的人可能都知道 DreamScene,那是 Windows Vista 的一个功能,它允许将视频序列(.dream 格式)渲染为桌面背景。还有一个名为 rainmeter 的工具,它允许您在桌面上以最上层、顶层和底层位置放置小部件/小工具/任何东西。此外,还有 winamp,我第一次看到 directx 在桌面图标后面渲染的程序。
这些工具有一个共同点,它们不能在 Windows 8 中实现,至少“在桌面图标下方绘图”的部分不能。
编辑
本文描述的方法也适用于 Windows 10。
它以前是如何工作的 (Windows XP, Vista, 7)
有一个窗口树。这个树包含当前桌面上所有当前显示/或隐藏的窗口,还有一个名为 Spy++ 的工具(Visual Studio -> 工具 -> Spy++),可以用来显示和导航该树。该工具是 Visual Studio 的一部分。
这个树的最后一个叶子是程序管理器。这个窗口代表整个 shell。在 Windows XP、Vista 和 7(Aero 关闭)中,这个程序管理器 (Progman) 包含一个窗口 (SysListView32
),它渲染桌面图标。因此,如果您将该程序管理器设置为您的父窗口,您就可以将自己定位在这些桌面图标的后面。 编辑: 滚动到下面的评论部分,Kristjan Skutta 的评论描述了如何在 Aero 开启的 Windows 7 中实现:https://codeproject.org.cn/Messages/5164145/Re-Windows.aspx
CodeProject 上有一篇关于在桌面上绘图(在桌面图标前面和后面)的精彩文章
- 桌面上的雪花飘落!第二部分 作者 Igor Tolmachev
但我绞尽脑汁,也找不到适用于 Windows 8 的方法。
Windows 8+ 的问题
Windows 7 和 Windows 8 非常相似。为了使 Windows XP 的方法适用于 Windows 7,您必须关闭 Aero 桌面。对于 Windows 8,您无法关闭 Aero,因此必须有另一种方法。
下图显示了 Windows 8 中窗口树的结构。
SysListView32
现在与程序管理器分离。这本身并不是一个大问题。
问题是桌面背景。它现在与桌面图标融合在一起。所以我们只能在所有东西(包括图标)的上面绘制,或者在所有东西(包括背景)的下面绘制。不可能在 Z 顺序中定位窗口,使其位于桌面图标和桌面壁纸之间。我尝试了所有位置。
寻找解决方案的方法
引导我找到本文中提出的解决方案的是个性化对话框。当您手动设置壁纸时,您不会看到突然的变化,而是使用平滑的淡入动画设置壁纸。在我看来,只有当系统能够以某种方式在桌面图标后面绘制时,这种动画才有可能,因为连续快速设置壁纸会非常慢且难看。
所以我设置了 Spy++,打开了个性化对话框并更改了壁纸。结果发现,当您更改桌面壁纸时,在包含桌面图标 (SysListView32
) 的 WorkerW
实例和桌面管理器之间创建了一个新的 WorkerW
窗口。
我获取了那个新的 WorkerW
窗口的句柄,并将其放入我的测试程序中,它终于能够在桌面图标后面,直接在壁纸上方绘制了!
一个问题仍然存在。当我关闭个性化对话框时,那个新的 WorkerW
窗口也随之关闭了。
我必须找到一种方法来触发这个 WorkerW
窗口的创建。
Spy++ 将该窗口报告为程序管理器的兄弟和子级,因此看起来是程序管理器创建了它。我使用 Spy++ 打开了程序管理器的消息监视,并找到了我正在寻找的东西。
在点击更改桌面壁纸后,程序管理器接收到一堆消息。第一个是用户定义的未记录消息。这闻起来像是“最近”添加的。
我扩展了测试程序,向程序管理器发送了完全相同的用户定义消息 (0x052C)。它果然引起了我所希望的。在收到消息后,程序管理器创建了 WorkerW
窗口。
所有准备就绪后,我编写了一个小型演示应用程序,演示了如何在桌面上绘图(在图标后面)以及如何将窗口放在桌面图标后面。
代码
获取程序管理器句柄
首先,我们通过查找 Progman
窗口的句柄开始。我们可以使用 Windows API 提供的 FindWindow
函数来完成此任务。
// Fetch the Progman window
IntPtr progman = W32.FindWindow("Progman", null);
向程序管理器发送消息
为了触发在桌面图标和壁纸之间创建 WorkerW
窗口,我们必须向程序管理器发送一条消息。这条消息是一个未记录的消息,所以除了 0x052C
之外,它没有花哨的 Windows API 名称。为了发送消息,我们使用 Windows API 方法 SendMessageTimeout
。
IntPtr result = IntPtr.Zero;
// Send 0x052C to Progman. This message directs Progman to spawn a
// WorkerW behind the desktop icons. If it is already there, nothing
// happens.
W32.SendMessageTimeout(progman,
0x052C,
new IntPtr(0),
IntPtr.Zero,
W32.SendMessageTimeoutFlags.SMTO_NORMAL,
1000,
out result);
获取新创建窗口的句柄
现在,我们必须获取那个新创建的 WorkerW
窗口的句柄。由于有多个标题为 "" 且类名为 "WorkerW
" 的窗口,我们必须按顺序遍历窗口树。这可以通过使用 EnumWindows
函数来完成。
EnumWindows
会为每个顶级窗口调用提供的 EnumWindowProc
。从那里,我们可以检查当前窗口是否包含名为 "SHELLDLL_DefView
" 的子窗口,这表示当前窗口代表桌面图标。然后我们取该窗口的下一个兄弟窗口。
// Spy++ output
// .....
// 0x00010190 "" WorkerW
// ...
// 0x000100EE "" SHELLDLL_DefView
// 0x000100F0 "FolderView" SysListView32
// 0x00100B8A "" WorkerW <-- This is the WorkerW instance we are after!
// 0x000100EC "Program Manager" Progman
IntPtr workerw = IntPtr.Zero;
// We enumerate all Windows, until we find one, that has the SHELLDLL_DefView
// as a child.
// If we found that window, we take its next sibling and assign it to workerw.
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);
演示 1:在图标和壁纸之间绘制图形
有了 workerw
句柄,有趣的事情就开始了。第一个演示是关于使用 System.Drawing
类来绘制一些东西。
此演示在桌面左上角绘制一个矩形。如果您使用多个显示器,请注意,桌面区域跨越所有显示器形成一个矩形,因此请确保您的左显示器已打开,并且您的显示器放置实际上将左上角映射到某个显示器,以防您有四个显示器,其中一个在另外三个上面。
注意:您在此层上绘制的所有内容都将保留在那里,直到您在其上绘制、使其失效或重置您的壁纸。
// Get the Device Context of the WorkerW
IntPtr dc = W32.GetDCEx(workerw, IntPtr.Zero, (W32.DeviceContextValues)0x403);
if (dc != IntPtr.Zero)
{
// Create a Graphics instance from the Device Context
using (Graphics g = Graphics.FromHdc(dc))
{
// Use the Graphics instance to draw a white rectangle in the upper
// left corner. In case you have more than one monitor think of the
// drawing area as a rectangle that spans across all monitors, and
// the 0,0 coordinate being in the upper left corner.
g.FillRectangle(new SolidBrush(Color.White), 0, 0, 500, 500);
}
// make sure to release the device context after use.
W32.ReleaseDC(workerw, dc);
}
演示 2:将 Windows 窗体放在桌面图标后面
此演示展示了如何将一个普通的 Windows 窗体放在桌面图标后面。本质上,这可以通过将 Windows 窗体的父级设置为我们的 WorkerW
窗口来完成。要设置窗体的父级,我们可以使用 SetParent
Windows API 函数。
注意:为了使此函数正常工作,窗体必须已经创建。form.Load
事件似乎是正确的放置位置。
为了一个简短的例子,我直接创建了窗体,没有使用“项目->添加 Windows 窗体...”对话框和设计器。
Form form = new Form();
form.Text = "Test Window";
form.Load += new EventHandler((s, e) =>
{
// Move the form right next to the in demo 1 drawn rectangle
form.Width = 500;
form.Height = 500;
form.Left = 500;
form.Top = 0;
// Add a randomly moving button to the form
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();
// This line makes the form a child of the WorkerW window,
// thus putting it behind the desktop icons and out of reach
// for any user input. The form will just be rendered, no
// keyboard or mouse input will reach it. You would have to use
// WH_KEYBOARD_LL and WH_MOUSE_LL hooks to capture mouse and
// keyboard input and redirect it to the windows form manually,
// but that's another story, to be told at a later time.
W32.SetParent(form.Handle, workerw);
});
// Start the Application Loop for the Form.
Application.Run(form);
您可能会注意到,一旦窗体的父级设置为 WorkerW
窗口,就无法与窗体交互。桌面不是设计为具有交互式子级的,因此所有关于鼠标移动、键盘输入等的事件都不会到达我们的窗体。
有一个办法可以解决这个问题。您可以订阅低级别的 WH_KEYBOARD_LL
和 WH_MOUSE_LL
事件,这也为键盘记录器和鼠标捕获软件所熟知。通过这些事件,您可以接收鼠标移动、点击和按键操作,无论它们发生在何处。您必须将这些消息转发到您的窗体并执行您自己的命中测试。
结论
一个命令统治一切 ;)
W32.SendMessageTimeout(W32.FindWindow("Progman", null),
0x052C,
new IntPtr(0),
IntPtr.Zero,
W32.SendMessageTimeoutFlags.SMTO_NORMAL,
1000,
out result);