文章中全部代码已开源,请访问 SENK001/DesktopShow
# 前言
很早之前用过一个动态壁纸软件,这个软件有一个功能非常好用,就是双击桌面空白处显示或隐藏桌面图标。这样既可以无遮挡的欣赏动态壁纸上的 “老婆”,又方便打开桌面上的应用快捷方式。但是后来换成了 Wallpaper Engine 没有这个功能,就想尝试自己实现一个。然后发现实现这个功能并不简单,因为要实现一个进程去监控另一个进程的消息,查阅网上的资料发现需要用到 Windows Hook
技术。 Windows Hook
技术有很多种,这里我们使用 dll 注入消息钩子的方法。
# 准备
因为消息钩子只在有消息循环的程序中才有效果,所以我在 Visual Studio 中创建了一个 Win32
项目,然后再在同一解决方案下创建一个动态链接库项目,并在 Win32 项目中引用动态链接库。
# 原理
- 创建一个
Windows Hook
,并设置钩子类型为WH_MOUSE
,钩子函数为MouseProc
。 - 创建一个
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); | |
} |
- 首先,在
WndProc
函数中,我们加载了动态链接库MouseHookDll.dll
,并获取了导出函数StartHook
和StopHook
的函数指针。如果加载失败,则返回 - 1。 - 接下来,我们根据不同的消息类型,开启和关闭钩子,并更新菜单。
- 最后,在
WM_DESTROY
消息中,我们释放了动态链接库和删除了通知图标。
# 参考资料
- Windows API 参考:https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/
- C++ HOOK 实现全局键盘钩子的详细过程:https://blog.csdn.net/qq_43851684/article/details/112759669
- 如何 HOOK 桌面窗口消息:https://www.cnblogs.com/vcerror/p/4289108.html
- windows API 创建系统托盘图标:https://www.cnblogs.com/vcerror/p/4289014.html