65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (59投票s)

2014年12月23日

CPOL

6分钟阅读

viewsIcon

211890

downloadIcon

4040

在 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 上有一篇关于在桌面上绘图(在桌面图标前面和后面)的精彩文章

但我绞尽脑汁,也找不到适用于 Windows 8 的方法。

Windows 8+ 的问题

Windows 7 和 Windows 8 非常相似。为了使 Windows XP 的方法适用于 Windows 7,您必须关闭 Aero 桌面。对于 Windows 8,您无法关闭 Aero,因此必须有另一种方法。

下图显示了 Windows 8 中窗口树的结构。

Spy++ showing the bottom section of the window tree

SysListView32 现在与程序管理器分离。这本身并不是一个大问题。
问题是桌面背景。它现在与桌面图标融合在一起。所以我们只能在所有东西(包括图标)的上面绘制,或者在所有东西(包括背景)的下面绘制。不可能在 Z 顺序中定位窗口,使其位于桌面图标和桌面壁纸之间。我尝试了所有位置。

寻找解决方案的方法

引导我找到本文中提出的解决方案的是个性化对话框。当您手动设置壁纸时,您不会看到突然的变化,而是使用平滑的淡入动画设置壁纸。在我看来,只有当系统能够以某种方式在桌面图标后面绘制时,这种动画才有可能,因为连续快速设置壁纸会非常慢且难看。

所以我设置了 Spy++,打开了个性化对话框并更改了壁纸。结果发现,当您更改桌面壁纸时,在包含桌面图标 (SysListView32) 的 WorkerW 实例和桌面管理器之间创建了一个新的 WorkerW 窗口。

Spy++ showing the bottom section of the window tree

我获取了那个新的 WorkerW 窗口的句柄,并将其放入我的测试程序中,它终于能够在桌面图标后面,直接在壁纸上方绘制了!

一个问题仍然存在。当我关闭个性化对话框时,那个新的 WorkerW 窗口也随之关闭了。

我必须找到一种方法来触发这个 WorkerW 窗口的创建。

Spy++ 将该窗口报告为程序管理器的兄弟和子级,因此看起来是程序管理器创建了它。我使用 Spy++ 打开了程序管理器的消息监视,并找到了我正在寻找的东西。

Spy++ showing the bottom section of the window tree

在点击更改桌面壁纸后,程序管理器接收到一堆消息。第一个是用户定义的未记录消息。这闻起来像是“最近”添加的。

我扩展了测试程序,向程序管理器发送了完全相同的用户定义消息 (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_LLWH_MOUSE_LL 事件,这也为键盘记录器和鼠标捕获软件所熟知。通过这些事件,您可以接收鼠标移动、点击和按键操作,无论它们发生在何处。您必须将这些消息转发到您的窗体并执行您自己的命中测试。

结论

一个命令统治一切 ;)

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