文章中全部代码已开源,请访问 SENK001/DesktopShow

# 前言

很早之前用过一个动态壁纸软件,这个软件有一个功能非常好用,就是双击桌面空白处显示或隐藏桌面图标。这样既可以无遮挡的欣赏动态壁纸上的 “老婆”,又方便打开桌面上的应用快捷方式。但是后来换成了 Wallpaper Engine 没有这个功能,就想尝试自己实现一个。然后发现实现这个功能并不简单,因为要实现一个进程去监控另一个进程的消息,查阅网上的资料发现需要用到 Windows Hook 技术。 Windows Hook 技术有很多种,这里我们使用 dll 注入消息钩子的方法。

# 准备

因为消息钩子只在有消息循环的程序中才有效果,所以我在 Visual Studio 中创建了一个 Win32 项目,然后再在同一解决方案下创建一个动态链接库项目,并在 Win32 项目中引用动态链接库。

# 原理

  1. 创建一个 Windows Hook ,并设置钩子类型为 WH_MOUSE ,钩子函数为 MouseProc
  2. 创建一个 MouseProc 函数,用于处理消息。

这里用到了一个重要的函数 SetWindowsHookEx ,函数原型如下:

HHOOK SetWindowsHookExW(
    int       idHook,
    HOOKPROC  lpfn,
    HINSTANCE hmod,
    DWORD     dwThreadId
);

# 实现

# 动态链接库编写

创建一个导出函数,创建鼠标钩子,这个函数在 Win32 应用程序中调用。

extern "C" _declspec(dllexport) BOOL StartHook()
{
    hHook = SetWindowsHookEx(WH_MOUSE, MouseProc, hInstance, 0);
    if (!hHook) return FALSE;
    return TRUE;
}

创建一个鼠标处理函数 MouseProc ,用于处理鼠标消息,这是一个回调函数。

LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    HWND hDesktop = FindDesktopWindow();// 桌面窗口句柄
    if (nCode >= 0) {
        if (wParam == WM_MBUTTONDOWN)
        {
            LPMOUSEHOOKSTRUCT pMouse = (LPMOUSEHOOKSTRUCT)lParam;
            if (pMouse->hwnd == hDesktop || pMouse->hwnd == GetParent(hDesktop))
            {
                if (IsWindowVisible(hDesktop))
                    ShowWindow(hDesktop, SW_HIDE);
                else 
                    ShowWindow(hDesktop, SW_SHOW);
            }
        }
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

FindDesktopWindow 函数用于获取桌面窗口的句柄, GetParent 函数用于获取桌面窗口的父窗口,即桌面窗口。

IsWindowVisible 函数用于判断窗口是否可见,如果可见则隐藏,否则显示。

CallNextHookEx 函数用于调用下一个钩子函数,如果没有下一个钩子函数,则返回 0。

FindDesktopWindow 函数不是我自己写的,是复制自 @vcerror 的这篇文章如何 HOOK 桌面窗口消息中的 FindShellWindow 函数,这里我就不贴代码了。

创建一个导出函数,用于卸载钩子,这个函数在 Win32 应用程序中调用。

extern "C" _declspec(dllexport) void StopHook()
{
    if (hHook) {
        UnhookWindowsHookEx(hHook);
        hHook = NULL;
    }
}

# Win32 应用程序编写

关于如何创建窗口,这里不细说,只给出 WndProc 函数

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    // 加载动态链接库
    if (!hLib) {
        hLib = LoadLibrary(L"MouseHookDll.dll");
        if (hLib == NULL)
        {
            MessageBox(NULL, L"加载动态链接库失败", L"错误", MB_OK | MB_ICONERROR);
            return -1;
        }
        StartHookFunc = reinterpret_cast<BOOL(*)()>(GetProcAddress(hLib, "StartHook"));
        StopHookFunc = reinterpret_cast<void(*)()>(GetProcAddress(hLib, "StopHook"));
        if (!StartHookFunc || !StopHookFunc)
        {
            FreeLibrary(hLib);
            return 1;
        }
    }
    switch (message)
    {
    case WM_CREATE:
        nid.cbSize = sizeof(nid);
        nid.hWnd = hWnd;
        nid.uID = 0;
        nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
        nid.uCallbackMessage = WM_USER;
        nid.hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_SMALL));
        lstrcpy(nid.szTip, szTitle);
        Shell_NotifyIcon(NIM_ADD, &nid);
        hMenu = CreatePopupMenu();// 生成菜单
        AppendMenu(hMenu, MF_STRING, IDC_START_HOOK, L"开始服务");// 添加菜单项
        AppendMenu(hMenu, MF_STRING, IDC_STOP_HOOK, L"暂停服务");
        AppendMenu(hMenu, MF_STRING | ((CheckStartup(hWnd)) ? MF_CHECKED : MF_UNCHECKED), IDC_SELFSTART, L"开机启动");
        AppendMenu(hMenu, MF_SEPARATOR, 0, NULL);
        AppendMenu(hMenu, MF_STRING, IDC_ABOUT, L"关于");
        AppendMenu(hMenu, MF_STRING, IDC_QUIT, L"退出");
        isHook = StartHookFunc();
        break;
    case WM_USER:
        if (lParam == WM_RBUTTONDOWN)
        {
            POINT pt;// 用于接收鼠标坐标
            int Select = IDC_START_HOOK;// 用于接收菜单选项返回值
            GetCursorPos(&pt);// 取鼠标坐标  
            ::SetForegroundWindow(hWnd);// 解决在菜单外单击左键菜单不消失的问题
            UpdateMenuItems(hWnd);// 更新菜单
            Select = TrackPopupMenu(hMenu, TPM_RETURNCMD, pt.x, pt.y, NULL, hWnd, NULL);// 显示菜单并获取选项 ID
            switch (Select)
            {
            case IDC_START_HOOK:
                isHook = StartHookFunc();
                break;
            case IDC_STOP_HOOK:
                StopHookFunc();
                isHook = FALSE;
                break;
            case IDC_SELFSTART:
                CheckStartup(hWnd) ? DisableStartup(hWnd) : EnableStartup(hWnd);// 启用或禁用开机启动
                break;
            case IDC_ABOUT:
                DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
                break;
            case IDC_QUIT:
                PostMessage(hWnd, WM_DESTROY, wParam, lParam);
                break;
            default:
                break;
            }
        }
        break;
    case WM_DESTROY:
        if(isHook) StopHookFunc();
        if(hLib) FreeLibrary(hLib);
        Shell_NotifyIcon(NIM_DELETE, &nid);
        CloseHandle(hMutex);
        PostQuitMessage(0);
        break;
    default:
        if (message == WM_TASKBARCREATED)
            SendMessage(hWnd, WM_CREATE, wParam, lParam);
        break;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}
  1. 首先,在 WndProc 函数中,我们加载了动态链接库 MouseHookDll.dll ,并获取了导出函数 StartHookStopHook 的函数指针。如果加载失败,则返回 - 1。
  2. 接下来,我们根据不同的消息类型,开启和关闭钩子,并更新菜单。
  3. 最后,在 WM_DESTROY 消息中,我们释放了动态链接库和删除了通知图标。

# 参考资料

  1. Windows API 参考:https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/
  2. C++ HOOK 实现全局键盘钩子的详细过程:https://blog.csdn.net/qq_43851684/article/details/112759669
  3. 如何 HOOK 桌面窗口消息:https://www.cnblogs.com/vcerror/p/4289108.html
  4. windows API 创建系统托盘图标:https://www.cnblogs.com/vcerror/p/4289014.html