目录
7.1 截获 WM_COPYDATA 消息(HOOK 途径)
7.3 通过 XAML Diagnostics 和 IsLands 访问界面元素(Hook Xaml 途径)
文章出处链接:[https://blog.csdn.net/qq_59075481/article/details/136240462]
前言
在《获取 Windows 系统托盘图标信息的最新方案(一)》中(下文简称 《最新方案(一)》),我们讨论了在 Win11 22H2 上获取系统托盘图标信息的方法,即拦截 Shell_TrayWnd 窗口的 WM_COPYDATA 消息。在《最新方案(一)》中,我们主要使用 Inline hook 重写 CTray::v_WndProc 函数,也就是窗口过程函数来拦截 WM_COPYDATA 消息。具体分析了两种注入 explorer 的实现方法:(1)创建挂起进程的远程线程注入;(2)模拟调试进程的 TLS 函数注入。本文我将分析通过 SetWindowsHookEx 实现消息钩子的角度分析如何拦截 WM_COPYDATA,该方法与拦截未导出的 CTray::v_WndProc 函数相比,将更加易于实现。
【注明】
(1)目前该系列分析的对 WM_COPYDATA 机制进行处理的方法依然适用于 Win11 22H2、 23H2 以及测试渠道的 24H2。(检测时间:2024.03.04, 2024.05.10, 2024.08.23)
(2)关于通知图标的安全隐藏问题有待未来研究 XAML 解决,可能涉及到 XAML 诊断相关技术;
(3)未来将更新窗口子类化代码,使得可以通过系统接口管理托盘图标状态或者拦截通知等。(2024.07.10)
系列文章列表:
编号 | 文章标题 | AID |
1 | 128594435 | |
2 | 获取 Windows 系统托盘图标信息的最新方案(一) | 136134195 |
3 | 获取 Windows 系统托盘图标信息的最新方案(二) | 136199670 |
4 | 获取 Windows 系统托盘图标信息的最新方案(三) | 136240462 |
5 | 对通知区域(Win 系统托盘)窗口层次的分析 | 128538050 |
一、原理解释
应用程序通过 Shell_NotifyIcon 函数向系统通知区域注册“通知图标”。 Shell_NotifyIcon 函数仅仅是将 NOTIFYICONDATA 结构封装到 COPYDATASTRUCT (CDS) 中,通过 WM_COPYDATA 消息向 explorer.exe 共享缓冲区的信息。此缓冲区传递的结构包括了 NOTIFYICONDATA 结构包含的全部信息、要处理的图标状态更改(Shell_NotifyIcon 函数的 dwMessage 参数)以及调用方的一部分信息(调用方窗口句柄、进程完整路径等)。 NOTIFYICONDATA 结构是通知图标的相关信息。
当了解到这些情况后,我们还可以通过 Spy++ 工具知道通知区域处理此类 WM_COPYDATA 消息的窗口是“Shell_TrayWnd”。所有这些,在第一篇中已经通过逆向工程和相关文献被分析出来。
为了便于获取所传递结构的成员,我解析了 UNICODE 版本的结构:
// 结构体的声明(UNICODE 字符集)
typedef struct _TRAY_ICON_DATAW {
DWORD Signature;
DWORD dwMessage; // dwMessage <-- Shell_NotifyIconW(DWORD dwMessage, ...)
DWORD cbSize;
DWORD hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
DWORD uIconID; // HICON hIcon; why it changes?
#if (NTDDI_VERSION < NTDDI_WIN2K)
WCHAR szTip[64];
#endif
#if (NTDDI_VERSION >= NTDDI_WIN2K)
WCHAR szTip[128];
DWORD dwState;
DWORD dwStateMask;
WCHAR szInfo[256];
#ifndef _SHELL_EXPORTS_INTERNALAPI_H_
union {
UINT uTimeout;
UINT uVersion; // used with NIM_SETVERSION, values 0, 3 and 4
} DUMMYUNIONNAME;
#endif
WCHAR szInfoTitle[64];
DWORD dwInfoFlags;
#endif
#if (NTDDI_VERSION >= NTDDI_WINXP)
GUID guidItem;
#endif
#if (NTDDI_VERSION >= NTDDI_VISTA)
HICON hBalloonIcon;
#endif
} TRAY_ICON_DATAW, * PTRAY_ICON_DATAW;
虽然此内部结构存在 UNICODE 和 ASCII 两种版本,但是当使用 ASCII 版本时,内部会进行隐式转换,发送时的消息结构还是 UNICODE 编码的。
结构的第一个成员是一个哨兵值(签名),因为 WM_COPYDATA 可能传递多种不同结构,而为了便于接收消息的 explorer 程序能够正确处理不同的情况,内部采用了签名来验证消息的格式。
而传递通知图标结构体信息时的签名信息为 0x34753423,也就是说,接收方会检查该值是否是 0x34753423 来决定是否调用相应的处理。
而结构的第二个成员则是和 Shell_NotifyIcon 函数绑定的参数 dwMessage,该参数指定了需要对通知图标进行的操作。这里有几个不同的操作,包含创建图标、修改图标、删除图标等,只需要指定 Shell_NotifyIcon 的文档所规定的常量。
结构接下来的成员则基本和 NOTIFYICONDATA 结构相对应,只不过对 hWnd 和 hIcon 成员有所变化,他们转为了由 DWORD 值存储。
微软提供的 Win32 WindowsHook 可以实现在消息到达目标窗口前/后进行捕获,其中CallWndProcHook 就可以在消息被窗口处理前捕获(但是无法修改或者阻止消息向目标窗口过程传递),而 GetMessageHook 只可以捕获通过 GetMessage 或者 PeekMessage 接收到窗口的消息,无法获取直接派送到窗口过程的消息,比如这里的 WM_COPYDAYA 就没法捕获。同时, GetMessageHook 不能在消息发送至窗口过程前获取消息和修改消息传递。
通过创建一个 CallWndProcHook 钩子例程,并利用 WM_COPYDATA 将发送至 Shell_TrayWnd 窗口的 WM_COPYDATA 转发至我们创建窗口。这将允许我们获取通知区域图标的注册或者状态更改信息。
在这里,我们需要注意一点,使用 SendMessage(严禁使用,除非你知道你在做什么) 和 SendMessageTimeout 转发消息均可能因阻滞导致一系列问题,尤其是它们配合 CallWndProcHook 使用时,必须考虑清楚需要为 SMTO 设置的超时时间间隔(本文代码默认设置的是 10 秒)。如果为了安全起见,比如你只需要获取消息,而不需要改变消息的处理模式,则推荐使用 GetMessageHook 获取消息并配合 PostMessage 转发消息。
完整工具程序(TrayNotifyCapturer)流程:获取 Shell_TrayWnd 窗口句柄和线程ID,利用窗口子类化,在任意应用程序调用 Shell_NotifyIcon 或者 Shell_NotifyIconGetRect 时,会向 Shell_TrayWnd 窗口发送 COPYDATASTRUCT(CDS) 消息,该消息在到达 Shell_TrayWnd 窗口前被我们的 HOOK 获取,当然除了这两个函数会发送 CDS 消息外,其他函数也可能发送 CDS 消息,而这些 CDS 消息的签名和结构体大小不同,由此可以进行过滤获以便于取我们需要的消息包。此外,程序窗口还可能发送其他非 CDS 消息,对于非 CDS 消息我们不做处理,要让他们通过并继续发送至 Shell_TrayWnd 窗口。从需要的 CDS 消息中可以获取通知图标信息和控制权,通过修改信息就可以实现对通知图标的修改。
通过 CallWndProc Hook 实现的消息转发程序的流程图如下图所示:
通过窗口子类化实现的完全拦截消息程序的流程图如下图所示:
提示:如果需要拦截或者修改消息,则需要将 CallWndProc Hook 替换为窗口子类化(未来实现)或者窗口过程挂钩(第一篇讲过),因为 CallWndProc Hook 仅能够有限地操作托盘图标的消息(如获取信息和模拟消息等,但不能拦截活动消息)!!!
07/10 更新——窗口子类化示例:
https://blog.csdn.net/qq_59075481/article/details/140334106.
二、实现 CallWndProcHook
SetWindowsHookEx 提供了多种钩子类型。 WH_GETMESSAGE 或者 WH_CALLWNDPROC 类钩子都可以实现对窗口消息的获取。但它们有所不同,使用 WH_CALLWNDPROC 钩子后,消息在到达处理消息的窗口过程之前就被修改后的窗口过程处理,这类似于窗口子类化。
为了防止消息阻滞,使用 SendMessageTimeout 发送消息给我们自定义的消息处理窗口时,必须合理设置超时时间。
使用静态函数 GetMessageProc 配合 WH_GETMESSAGE 类钩子,可以拦截通过 GetMessage 和 PeekMessage 消息循环获取的窗口消息,但是无法获取直接发送到窗口过程的系统消息和用户消息。需要注意的是,这些捕获的消息不能够在消息被原始窗口处理前进行处理。
static LRESULT CALLBACK GetMessageProc(int code, WPARAM wParam, LPARAM lParam)
{
if (code == HC_ACTION)
{
PCWPSTRUCT pMsg = (PCWPSTRUCT)lParam;
SHELLWND_MAG lpTrayData = { 0 };
COPYDATASTRUCT lpSevCDS = { 0 };
DWORD_PTR lpdwResult = 0;
lpTrayData.hMsgWnd = pMsg->hwnd;
lpTrayData.message = pMsg->message;
lpTrayData.wParam = pMsg->wParam;
lpTrayData.lParam = pMsg->lParam;
// 填充 COPYDATASTRUCT 结构体
lpSevCDS.dwData = WM_NotifyGetMessage;
lpSevCDS.cbData = sizeof(SHELLWND_MAG);
lpSevCDS.lpData = &lpTrayData;
SendMessageTimeoutW(g_hNotifyWnd,
WM_COPYDATA, (WPARAM)wParam,
(LPARAM)&lpSevCDS, SMTO_NOTIMEOUTIFNOTHUNG, 0x0A, &lpdwResult);
//char szBuf[MAX_PATH] = {0};
//_snprintf_s(szBuf, MAX_PATH, "GetMessage Handle: 0x%08X PostMsg: %s(%04X), wParam: %08X, lParam: %08X\n",
// pMsg->hwnd, GetMsgStringA(pMsg->message), pMsg->message, (int)pMsg->wParam, (int)pMsg->lParam);
//OutputDebugStringA(szBuf);
}
return CallNextHookEx(g_hGetMessageHook, code, wParam, lParam);
}
使用静态函数 CallWndProc 配合 WH_CALLWNDPROC 类钩子拦截 WM_COPYDATA 等特殊消息结构。
在 HOOK 窗口过程或者消息时,拦截到的 wParam 是调用方窗口句柄,可以转为 DWORD 值或 HWND。lParam 则可能指向 CDS 结构体,这里需要检查 uMsg (message) 是否是 WM_COPYDATA。除此之外,还可以检查签名信息来进一步确认是否是传递通知图标信息的结构体。
通过转发消息到 g_hNotifyWnd 窗口,我们就可以作为中间人处理消息。
static LRESULT CALLBACK CallWndProc(int code, WPARAM wParam, LPARAM lParam)
{
if (code == HC_ACTION)
{
PCWPSTRUCT pMsg = (PCWPSTRUCT)lParam;
if (pMsg->hwnd == g_hCaptureWnd && pMsg->message == WM_COPYDATA)
{
PTRAY_ICON_DATAW lpTrayData = nullptr;
COPYDATASTRUCT* lpShellCDS =
(COPYDATASTRUCT*)pMsg->lParam;
COPYDATASTRUCT lpSevCDS = { 0 };
DWORD_PTR lpdwResult = 0;
if (lpShellCDS->dwData == 1) // 判断是否是 Shell_NotifyIcon 调用
{
lpTrayData = (TRAY_ICON_DATAW*)lpShellCDS->lpData;
if (lpTrayData->Signature == 0x34753423) // 判断是否是 NOTIFYICONDATA 结构体封送过程
{
// 填充 COPYDATASTRUCT 结构体
lpSevCDS.dwData = WM_NotifyCallWndProc;
lpSevCDS.cbData = sizeof(TRAY_ICON_DATAW);
lpSevCDS.lpData = lpTrayData;
// 发送消息到我们自己的窗口(10 秒)
SendMessageTimeoutW(g_hNotifyWnd,
WM_COPYDATA, (WPARAM)pMsg->wParam,
(LPARAM)&lpSevCDS, SMTO_NOTIMEOUTIFNOTHUNG, 0x0A, &lpdwResult);
}
}
//char szBuf[MAX_PATH] = {0};
//_snprintf_s(szBuf, MAX_PATH, "CallWndProc Handle: 0x%08X SendMsg: %s(%04X), wParam: %08X, lParam: %08X\n",
// pMsg->hwnd, GetMsgStringA(pMsg->message), pMsg->message, (int)pMsg->wParam, (int)pMsg->lParam);
//OutputDebugStringA(szBuf);
}
}
return CallNextHookEx(g_hCallWndProcHook, code, wParam, lParam);
}
使用静态函数 CBTProc 配合 WH_CBT 类钩子即可拦截窗口创建/销毁等状态消息(可以用于监视 explorer 相应的窗口是否创建)。
这个函数能比较多地获取窗口初始化前期的信息,可以在窗口创建的过程中拦截并获取创建窗口的结构信息。其中就包含调用 CreateWindowEx 创建窗口时的参数。
static LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
if (nCode == HCBT_ACTIVATE) //Called when the application window is activated
{
::PostMessage(g_hNotifyWnd, WM_NotifyActivate, wParam, NULL);
}
else if (nCode == HCBT_SETFOCUS)
{
::PostMessage(g_hNotifyWnd, WM_NotifyFocus, wParam, NULL);
}
else if (nCode == HCBT_DESTROYWND) //Called when the application window is destroyed
{
}
}
return CallNextHookEx(g_hCBTHook, nCode, wParam, lParam);
}
三、安装钩子例程
提示: WH_CALLWNDPROC 只能获取消息但不能拦截或者修改消息,WH_GETMESSAGE 不能够获取到部分窗口的消息。如果要修改消息传递,正确的方法应该使用窗口子类化和 IATHOOK。
使用 SetWindowsHookEx 并指定第三个参数是与挂钩例程绑定的消息窗口的线程,必须指定要挂勾的窗口的线程,否则将尝试在所有接收消息的窗口线程安装消息钩子(第三个参数为 NULL 时)。
WH_CALLWNDPROC 钩子安装的方式如下所示。首先通过 GetWindowThreadProcessId 并根据 Shell_TrayWnd 窗口句柄获取对应窗口线程的 Id。然后使用 SetWindowsHookEx 安装钩子。
DLL_EXPORT bool InstallCallWndProcHook(HWND hNotifyWnd, HWND hCaptureWnd)
{
g_hNotifyWnd = hNotifyWnd;
g_hCaptureWnd = hCaptureWnd;
if (!g_hCallWndProcHook)
{
DWORD dwThreadId = ::GetWindowThreadProcessId(g_hCaptureWnd, NULL);
g_hCallWndProcHook = SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)CallWndProc, g_hInstance, dwThreadId);
if (g_hCallWndProcHook)
{
OutputDebugStringA("Hook CallWndProc succeed\n");
return true;
}
else
{
DWORD dwError = GetLastError();
char szError[MAX_PATH];
_snprintf_s(szError, MAX_PATH, "Hook CallWndProc failed, error = %u\n", dwError);
OutputDebugStringA(szError);
}
}
return false;
}
四、创建消息处理窗口
在我们的程序中需要创建消息处理窗口来处理钩子模块转发至我们进程的消息数据。
LRESULT CALLBACK WindowProc(
HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam);
int main() {
_wsetlocale(LC_ALL, L".UTF8"); // 设置代码页以支持中文
SetConsoleTitleW(L"ConsoleShowNofifyMsg");
// 创建窗口类
WNDCLASS wc = {};
wc.lpfnWndProc = WindowProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = L"ShellMsgGetMessageWindowClass";
RegisterClass(&wc);
// 创建窗口
HWND hwnd = CreateWindowExW(
WS_EX_LAYERED |
WS_EX_TRANSPARENT |
WS_EX_TOOLWINDOW,
wc.lpszClassName,
L"ShellMsgGetMessageWindow",
0, 0, 0,
0, 0,
NULL, NULL,
GetModuleHandle(NULL),
NULL);
if (hwnd == NULL) {
std::cerr << "Failed to create window." << std::endl;
return 1;
}
// 设置窗口透明度
SetLayeredWindowAttributes(hwnd, RGB(0, 0, 0), 1, LWA_ALPHA);
// 显示窗口
ShowWindow(hwnd, SW_SHOWDEFAULT);
// 启用窗口过程钩子
if (!OnInstallHookNotifyWndProc(hwnd))
{
CloseWindow(hwnd);
return -1;
}
// 消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
由于是要对 Shell_TrayWnd 窗口进行挂钩处理,所以我们在挂钩处理程序中这样写:
// 安装 Win32 钩子
BOOL OnInstallHookNotifyWndProc(HWND hDlgWnd)
{
bool m_bCallWndProcHooked = false;
HWND hShellWnd = ::FindWindow(TEXT("Shell_TrayWnd"), nullptr);
if (!hShellWnd || !::IsWindow(hShellWnd))
{
MessageBox(NULL, TEXT("Not found Shell_TrayWnd."),
TEXT("No!!!"), MB_OK | MB_ICONWARNING);
return FALSE;
}
m_bCallWndProcHooked = InstallCallWndProcHook(hDlgWnd, hShellWnd);
if (m_bCallWndProcHooked)
{
wprintf(L"Hook CallWndProc succeed\r\n");
return TRUE;
}
else
{
wprintf(L"Hook CallWndProc failed\r\n");
return FALSE;
}
}
创建窗口以及挂钩均完成后,窗口需要开始接收消息。接收消息的窗口过程函数如下:
// 消息窗口过程函数
LRESULT CALLBACK WindowProc(
HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
OnUninstallHookNotifyWndProc();
PostQuitMessage(0);
return 0;
case WM_COPYDATA:
{
COPYDATASTRUCT* lpReceiveCDS = (COPYDATASTRUCT*)lParam;
PTRAY_ICON_DATAW lpNotifyData = nullptr;
if (lpReceiveCDS == nullptr ||
IsBadReadPtr(lpReceiveCDS, sizeof(TRAY_ICON_DATAW)) == TRUE)
{
break;
}
// 测试时,只实现了 WM_NotifyCallWndProc 钩子的消息处理
if (lpReceiveCDS->dwData == WM_NotifyCallWndProc
&& lpReceiveCDS->cbData == sizeof(TRAY_ICON_DATAW))
{
lpNotifyData = (TRAY_ICON_DATAW*)lpReceiveCDS->lpData;
// 输出结果
wprintf(L"CTray-NotifyIconMsg:[%ws]:[%ws];\n",
DMSG2TEXT(lpNotifyData->dwMessage), HWND2TEXT((HWND)wParam));
if ((lpNotifyData->uFlags & NIF_INFO) != 0)
{
wprintf(L"Tip[%ws], szInfoParam:\n",
lpNotifyData->szTip);
wprintf(L"InfoTitle[%ws], Info[%ws], InfoFlags[%ws];\n",
lpNotifyData->szInfoTitle, lpNotifyData->szInfo,
NIIF2TEXT(lpNotifyData->dwInfoFlags));
}
else if ((lpNotifyData->uFlags & NIF_TIP) != 0)
{
wprintf(L"Tip[%ws], non-szInfo;\n", lpNotifyData->szTip);
}
else {
wprintf(L"non-szTip, non-szInfo;\n");
}
}
break;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
程序编译完成后运行效果如下图所示:
在早期系统,如 Win 7 上运行效果:
五、完整代码和注意事项
注意事项:
(1) 该程序如果要实现获取全部图标的功能,必须在资源管理器创建窗口(推荐使用 WH_CBT 钩子)的时候就执行 WH_CALLWNDPROC 钩子对 WM_COPYDATA 消息进行转发处理。并把工具注册为系统开机自启动服务,这样就可以在 explorer 初始化时无缝衔接消息的处理过程。我给出的代码中 WH_CBT (钩子模块中有 CBT 挂钩的样例,但处理端没有去实现)和服务的部分由于时间匆忙暂未实施,不过理论上一定是可行的。
(2) 中文处理问题:程序面临着含中文字符的缓冲区处理问题,这个比较难解决。因为我一开始用的是 “_wsetlocale(LC_ALL, L".UTF8"); // 设置代码页以支持中文” 但是发现,在回溯到 Win8.1 虚拟机时,就出现了该语句无效的情况,会导致程序出错。我的解决方法就是改为 “_wsetlocale(LC_ALL, L"chs"); // 设置代码页以支持中文” 并且不使用 std::wcout 改为使用 std::cout 输出文本,类似地 sprintf,而不用 wsprinf。至于最终怎么解决,就仁者见仁智者见智了。
完整的钩子模块代码:
[文件: hookcore.h]
#pragma once
#ifdef SHELLMSGHOOK_EXPORTS
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif
enum NotifyMsg
{
WM_NotifyActivate = WM_APP + 1,
WM_NotifyFocus,
WM_NotifyCallWndProc,
WM_NotifyGetMessage,
};
// x64 结构体的声明
typedef struct _TRAY_ICON_DATAW {
DWORD Signature;
DWORD dwMessage; // dwMessage <-- Shell_NotifyIconW(DWORD dwMessage, ...)
DWORD cbSize;
DWORD hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
DWORD uIconID; // HICON hIcon; why it changes?
#if (NTDDI_VERSION < NTDDI_WIN2K)
WCHAR szTip[64];
#endif
#if (NTDDI_VERSION >= NTDDI_WIN2K)
WCHAR szTip[128];
DWORD dwState;
DWORD dwStateMask;
WCHAR szInfo[256];
#ifndef _SHELL_EXPORTS_INTERNALAPI_H_
union {
UINT uTimeout;
UINT uVersion; // used with NIM_SETVERSION, values 0, 3 and 4
} DUMMYUNIONNAME;
#endif
WCHAR szInfoTitle[64];
DWORD dwInfoFlags;
#endif
#if (NTDDI_VERSION >= NTDDI_WINXP)
GUID guidItem;
#endif
#if (NTDDI_VERSION >= NTDDI_VISTA)
HICON hBalloonIcon;
#endif
} TRAY_ICON_DATAW, * PTRAY_ICON_DATAW;
typedef struct __SHELLWND_MAG
{
LPARAM lParam;
WPARAM wParam;
UINT message;
HWND hMsgWnd;
}SHELLWND_MAG, PSHELLWND_MAG;
#ifdef __cplusplus
extern "C"
{
#endif
DLL_EXPORT bool InstallCBTHook(HWND hNotifyWnd);
DLL_EXPORT bool UninstallCBTHook();
DLL_EXPORT bool InstallCallWndProcHook(HWND hNotifyWnd, HWND hCaptureWnd);
DLL_EXPORT bool UninstallCallWndProcHook();
DLL_EXPORT bool InstallGetMessageHook(HWND hNotifyWnd, HWND hCaptureWnd);
DLL_EXPORT bool UninstallGetMessageHook();
#ifdef __cplusplus
}
#endif
[文件: dllmain.cpp]
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
//#include <windows.h>
#include <CommCtrl.h>
#include <commdlg.h>
#include <Richedit.h>
#include <Ime.h>
#include <shellapi.h>
#include <dde.h>
#include <stdio.h>
#include <map>
#include "hookcore.h"
//Initialized Data to be shared with all instance of the dll
#pragma data_seg("Shared")
HWND g_hNotifyWnd = NULL;
HWND g_hCaptureWnd = NULL;
HINSTANCE g_hInstance = NULL;
HHOOK g_hCBTHook = NULL;
HHOOK g_hCallWndProcHook = NULL;
HHOOK g_hGetMessageHook = NULL;
#pragma data_seg()
// Initialised data End of data share
#pragma comment(linker,"/section:Shared,RWS")
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
g_hInstance = hModule;
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
#ifdef __cplusplus
extern "C"
{
#endif
static LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
if (nCode == HCBT_ACTIVATE) //Called when the application window is activated
{
::PostMessage(g_hNotifyWnd, WM_NotifyActivate, wParam, NULL);
}
else if (nCode == HCBT_SETFOCUS)
{
::PostMessage(g_hNotifyWnd, WM_NotifyFocus, wParam, NULL);
}
else if (nCode == HCBT_DESTROYWND) //Called when the application window is destroyed
{
}
}
return CallNextHookEx(g_hCBTHook, nCode, wParam, lParam);
}
DLL_EXPORT bool InstallCBTHook(HWND hNotifyWnd)
{
g_hNotifyWnd = hNotifyWnd;
if (!g_hCBTHook)
{
g_hCBTHook = SetWindowsHookEx(WH_CBT, (HOOKPROC)CBTProc, g_hInstance, 0);
if (g_hCBTHook)
{
OutputDebugStringA("Hook CBT succeed\n");
return true;
}
else
{
DWORD dwError = GetLastError();
char szError[MAX_PATH];
_snprintf_s(szError, MAX_PATH, "Hook CBT failed, error = %u\n", dwError);
OutputDebugStringA(szError);
}
}
return false;
}
DLL_EXPORT bool UninstallCBTHook()
{
if (g_hCBTHook)
{
UnhookWindowsHookEx(g_hCBTHook);
g_hCBTHook = NULL;
OutputDebugStringA("Uninstall CBT Hook\n");
}
return true;
}
//note:
//CallWndProc will be executed in the process which myhook.dll injected, not the MySpy process
static LRESULT CALLBACK CallWndProc(int code, WPARAM wParam, LPARAM lParam)
{
if (code == HC_ACTION)
{
PCWPSTRUCT pMsg = (PCWPSTRUCT)lParam;
if (pMsg->hwnd == g_hCaptureWnd && pMsg->message == WM_COPYDATA)
{
PTRAY_ICON_DATAW lpTrayData = nullptr;
COPYDATASTRUCT* lpShellCDS =
(COPYDATASTRUCT*)pMsg->lParam;
COPYDATASTRUCT lpSevCDS = { 0 };
DWORD_PTR lpdwResult = 0;
if (lpShellCDS->dwData == 1) // 判断是否是 Shell_NotifyIcon 调用
{
lpTrayData = (TRAY_ICON_DATAW*)lpShellCDS->lpData;
if (lpTrayData->Signature == 0x34753423) // 判断是否是 NOTIFYICONDATA 结构体封送过程
{
// 填充 COPYDATASTRUCT 结构体
lpSevCDS.dwData = WM_NotifyCallWndProc;
lpSevCDS.cbData = sizeof(TRAY_ICON_DATAW);
lpSevCDS.lpData = lpTrayData;
// 发送消息到我们自己的窗口(10 秒)
SendMessageTimeoutW(g_hNotifyWnd,
WM_COPYDATA, (WPARAM)pMsg->wParam,
(LPARAM)&lpSevCDS, SMTO_NOTIMEOUTIFNOTHUNG, 0x0A, &lpdwResult);
}
}
//char szBuf[MAX_PATH] = {0};
//_snprintf_s(szBuf, MAX_PATH, "CallWndProc Handle: 0x%08X SendMsg: %s(%04X), wParam: %08X, lParam: %08X\n",
// pMsg->hwnd, GetMsgStringA(pMsg->message), pMsg->message, (int)pMsg->wParam, (int)pMsg->lParam);
//OutputDebugStringA(szBuf);
}
}
return CallNextHookEx(g_hCallWndProcHook, code, wParam, lParam);
}
DLL_EXPORT bool InstallCallWndProcHook(HWND hNotifyWnd, HWND hCaptureWnd)
{
g_hNotifyWnd = hNotifyWnd;
g_hCaptureWnd = hCaptureWnd;
if (!g_hCallWndProcHook)
{
DWORD dwThreadId = ::GetWindowThreadProcessId(g_hCaptureWnd, NULL);
g_hCallWndProcHook = SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)CallWndProc, g_hInstance, dwThreadId);
if (g_hCallWndProcHook)
{
OutputDebugStringA("Hook CallWndProc succeed\n");
return true;
}
else
{
DWORD dwError = GetLastError();
char szError[MAX_PATH];
_snprintf_s(szError, MAX_PATH, "Hook CallWndProc failed, error = %u\n", dwError);
OutputDebugStringA(szError);
}
}
return false;
}
DLL_EXPORT bool UninstallCallWndProcHook()
{
if (g_hCallWndProcHook)
{
UnhookWindowsHookEx(g_hCallWndProcHook);
g_hCallWndProcHook = NULL;
OutputDebugStringA("Uninstall CallWndProc Hook\n");
}
return true;
}
//note:
//CallWndProc will be executed in the process which myhook.dll injected, not the MySpy process
static LRESULT CALLBACK GetMessageProc(int code, WPARAM wParam, LPARAM lParam)
{
if (code == HC_ACTION)
{
PCWPSTRUCT pMsg = (PCWPSTRUCT)lParam;
SHELLWND_MAG lpTrayData = { 0 };
COPYDATASTRUCT lpSevCDS = { 0 };
DWORD_PTR lpdwResult = 0;
lpTrayData.hMsgWnd = pMsg->hwnd;
lpTrayData.message = pMsg->message;
lpTrayData.wParam = pMsg->wParam;
lpTrayData.lParam = pMsg->lParam;
// 填充 COPYDATASTRUCT 结构体
lpSevCDS.dwData = WM_NotifyGetMessage;
lpSevCDS.cbData = sizeof(SHELLWND_MAG);
lpSevCDS.lpData = &lpTrayData;
SendMessageTimeoutW(g_hNotifyWnd,
WM_COPYDATA, (WPARAM)wParam,
(LPARAM)&lpSevCDS, SMTO_NOTIMEOUTIFNOTHUNG, 0x0A, &lpdwResult);
//char szBuf[MAX_PATH] = {0};
//_snprintf_s(szBuf, MAX_PATH, "GetMessage Handle: 0x%08X PostMsg: %s(%04X), wParam: %08X, lParam: %08X\n",
// pMsg->hwnd, GetMsgStringA(pMsg->message), pMsg->message, (int)pMsg->wParam, (int)pMsg->lParam);
//OutputDebugStringA(szBuf);
}
return CallNextHookEx(g_hGetMessageHook, code, wParam, lParam);
}
DLL_EXPORT bool InstallGetMessageHook(HWND hNotifyWnd, HWND hCaptureWnd)
{
g_hNotifyWnd = hNotifyWnd;
g_hCaptureWnd = hCaptureWnd;
if (!g_hGetMessageHook)
{
DWORD dwThreadId = ::GetWindowThreadProcessId(g_hCaptureWnd, NULL);
g_hGetMessageHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMessageProc, g_hInstance, dwThreadId);
if (g_hGetMessageHook)
{
OutputDebugStringA("Hook GetMessage succeed\n");
return true;
}
else
{
DWORD dwError = GetLastError();
char szError[MAX_PATH];
_snprintf_s(szError, MAX_PATH, "Hook GetMessage failed, error = %u\n", dwError);
OutputDebugStringA(szError);
}
}
return false;
}
DLL_EXPORT bool UninstallGetMessageHook()
{
if (g_hGetMessageHook)
{
UnhookWindowsHookEx(g_hGetMessageHook);
g_hGetMessageHook = NULL;
OutputDebugStringA("Uninstall GetMessage Hook\n");
}
return true;
}
}
完整的模块注入和消息接收端代码:
// ConsoleShowMsg.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <windows.h>
#include <iostream>
#include "../ShellMsgHook/hookcore.h" // 钩子模块的头文件
// 钩子模块的 lib 文件
#if (NDEBUG) && (_WIN64)
#pragma comment(lib, "../x64/Release/ShellMsgHook.lib")
#else
#pragma comment(lib, "../x64/Debug/ShellMsgHook.lib")
#endif // NDEBUG
// 一些宏定义
#define NIM_ADD 0x00000000
#define NIM_MODIFY 0x00000001
#define NIM_DELETE 0x00000002
#define NIM_SETFOCUS 0x00000003
#define NIM_SETVERSION 0x00000004
#define NIF_TIP 0x00000004
#define NIF_INFO 0x00000010
// Notify Icon Infotip flags
#define NIIF_NONE 0x00000000
#define NIIF_INFO 0x00000001
#define NIIF_WARNING 0x00000002
#define NIIF_ERROR 0x00000003
#define NIIF_USER 0x00000004
#define NIIF_ICON_MASK 0x0000000F
#define NIIF_NOSOUND 0x00000010
#define NIIF_LARGE_ICON 0x00000020
#define NIIF_RESPECT_QUIET_TIME 0x00000080
// 格式化输出相关函数的声明
std::wstring make_hwnd_text(HWND hwnd);
std::wstring make_snmsg_text(DWORD dwMessage);
std::wstring make_infoflag_text(DWORD dwInfoFlags);
// 方便于调用格式化输出函数
#define HWND2TEXT(hwnd) make_hwnd_text(hwnd).c_str()
#define DMSG2TEXT(dwMessage) make_snmsg_text(dwMessage).c_str()
#define NIIF2TEXT(dwInfoFlags) make_infoflag_text(dwInfoFlags).c_str()
// 其他函数的声明
BOOL OnInstallHookNotifyWndProc(HWND hDlgWnd);
void OnUninstallHookNotifyWndProc();
LRESULT CALLBACK WindowProc(
HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam);
int main() {
setlocale(LC_ALL, "zh-CN"); // 设置代码页以支持中文,原本用的是 _wsetlocale(LC_ALL, L".UTF8")
SetConsoleTitleW(L"ConsoleShowNofifyMsg");
// 创建窗口类
WNDCLASS wc = {};
wc.lpfnWndProc = WindowProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = L"ShellMsgGetMessageWindowClass";
RegisterClass(&wc);
// 创建窗口
HWND hwnd = CreateWindowExW(
WS_EX_LAYERED |
WS_EX_TRANSPARENT |
WS_EX_TOOLWINDOW,
wc.lpszClassName,
L"ShellMsgGetMessageWindow",
0, 0, 0,
0, 0,
NULL, NULL,
GetModuleHandle(NULL),
NULL);
if (hwnd == NULL) {
std::cerr << "Failed to create window." << std::endl;
return 1;
}
// 设置窗口透明度
SetLayeredWindowAttributes(hwnd, RGB(0, 0, 0), 1, LWA_ALPHA);
// 显示窗口
ShowWindow(hwnd, SW_SHOWDEFAULT);
// 启用窗口过程钩子
if (!OnInstallHookNotifyWndProc(hwnd))
{
CloseWindow(hwnd);
return -1;
}
// 消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
// 消息窗口过程函数
LRESULT CALLBACK WindowProc(
HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
OnUninstallHookNotifyWndProc();
PostQuitMessage(0);
return 0;
case WM_COPYDATA:
{
COPYDATASTRUCT* lpReceiveCDS = (COPYDATASTRUCT*)lParam;
PTRAY_ICON_DATAW lpNotifyData = nullptr;
if (lpReceiveCDS == nullptr ||
IsBadReadPtr(lpReceiveCDS, sizeof(TRAY_ICON_DATAW)) == TRUE)
{
break;
}
// 测试时,只实现了 WM_NotifyCallWndProc 钩子的消息处理
if (lpReceiveCDS->dwData == WM_NotifyCallWndProc
&& lpReceiveCDS->cbData == sizeof(TRAY_ICON_DATAW))
{
lpNotifyData = (TRAY_ICON_DATAW*)lpReceiveCDS->lpData;
// 输出结果
wprintf(L"CTray-NotifyIconMsg:[%ws]:[%ws];\n",
DMSG2TEXT(lpNotifyData->dwMessage), HWND2TEXT((HWND)wParam));
if ((lpNotifyData->uFlags & NIF_INFO) != 0)
{
wprintf(L"Tip[%ws], szInfoParam:\n",
lpNotifyData->szTip);
wprintf(L"InfoTitle[%ws], Info[%ws], InfoFlags[%ws];\n",
lpNotifyData->szInfoTitle, lpNotifyData->szInfo,
NIIF2TEXT(lpNotifyData->dwInfoFlags));
}
else if ((lpNotifyData->uFlags & NIF_TIP) != 0)
{
wprintf(L"Tip[%ws], non-szInfo;\n", lpNotifyData->szTip);
}
else {
wprintf(L"non-szTip, non-szInfo;\n");
}
}
break;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
// 安装 Win32 钩子
BOOL OnInstallHookNotifyWndProc(HWND hDlgWnd)
{
bool m_bCallWndProcHooked = false;
HWND hShellWnd = ::FindWindow(TEXT("Shell_TrayWnd"), nullptr);
if (!hShellWnd || !::IsWindow(hShellWnd))
{
MessageBox(NULL, TEXT("Not found Shell_TrayWnd."),
TEXT("No!!!"), MB_OK | MB_ICONWARNING);
return FALSE;
}
m_bCallWndProcHooked = InstallCallWndProcHook(hDlgWnd, hShellWnd);
if (m_bCallWndProcHooked)
{
wprintf(L"Hook CallWndProc succeed\r\n");
return TRUE;
}
else
{
wprintf(L"Hook CallWndProc failed\r\n");
return FALSE;
}
}
// 卸载 Win32 钩子
void OnUninstallHookNotifyWndProc()
{
UninstallCallWndProcHook();
wprintf(L"Unhook CallWndProc\r\n");
}
// 窗口句柄转换字符串的函数
std::wstring make_hwnd_text(HWND hwnd)
{
wchar_t buf[25];
wsprintfW(buf, L"HWND:0x%I64X", (UINT64)hwnd);
return buf;
}
// dwMessage 参数转换为已知参数字符串
std::wstring make_snmsg_text(DWORD dwMessage)
{
#define CHECK_DMSG(dwMessage, var) if (dwMessage == var) return L#var;
CHECK_DMSG(dwMessage, NIM_ADD);
CHECK_DMSG(dwMessage, NIM_MODIFY);
CHECK_DMSG(dwMessage, NIM_DELETE);
CHECK_DMSG(dwMessage, NIM_SETFOCUS);
CHECK_DMSG(dwMessage, NIM_SETVERSION);
wchar_t buf[25];
wsprintfW(buf, L"Message:%u", dwMessage);
return buf;
#undef CHECK_HWND
}
// dwInfoFlags 参数转换为已知参数字符串
std::wstring make_infoflag_text(DWORD dwInfoFlags)
{
#define CHECK_DMSG(dwInfoFlags, var) if (dwInfoFlags == var) return L#var;
CHECK_DMSG(dwInfoFlags, NIIF_NONE);
CHECK_DMSG(dwInfoFlags, NIIF_INFO);
CHECK_DMSG(dwInfoFlags, NIIF_WARNING);
CHECK_DMSG(dwInfoFlags, NIIF_ERROR);
CHECK_DMSG(dwInfoFlags, NIIF_USER);
CHECK_DMSG(dwInfoFlags, NIIF_ICON_MASK);
CHECK_DMSG(dwInfoFlags, NIIF_NOSOUND);
CHECK_DMSG(dwInfoFlags, NIIF_LARGE_ICON);
CHECK_DMSG(dwInfoFlags, NIIF_RESPECT_QUIET_TIME);
wchar_t buf[25];
wsprintfW(buf, L"Message:%u", dwInfoFlags);
return buf;
#undef CHECK_HWND
}
格式化文本的方式其实和《最新方案(一)》里面的类似。
六、判断指定图标是否正在 “闪烁”
(本节于 2024/05/04 补充)
图标状态的更改包括图标创建、修改和删除,这和 dwMessage 有关。
dwMessage 支持的数值 | 表示的功能 |
NIM_ADD (0x00000000) | 将图标添加到状态区域。 |
NIM_MODIFY (0x00000001) | 修改状态区域中的图标。 |
NIM_DELETE (0x00000002) | 从状态区域中删除图标。 |
NIM_SETFOCUS (0x00000003) | 将焦点返回到任务栏通知区域。 |
NIM_SETVERSION (0x00000004) | 指示通知区域按照 lpdata 所指向结构的 uVersion 成员中指定的版本号的行为。 版本号指定可识别的成员。 |
大部分应用对他们的通知图标的管理都只能遵守这个规范。下面以 “图标闪烁” 为例解释一种中间状态。
经常使用一些通讯软件的人会知道,通知区域图标的 “闪烁” 意味着有新的消息或者来电。“闪烁”并不在上面列出的状态列表上面,那么,这个 “闪烁” 到底是如何实现的呢?
“闪烁” 是通过设置计时器,不断地调用 Shell_NotifyIcon 发送 NIM_MODIFY,在其中有一个巧妙的循环,实现 “闪烁” 效果。
下图展示了当 “微信” 有新消息时,该应用调用 Shell_NotifyIcon 函数并进行的动作。(NIM_ADD 和 NIM_SETVERSION 是初始化微信时候它注册通知图标的,后面的 NIM_MODIFY 才是发生即将闪烁时的动作)
消息结构中的 HICON hIcon(或者 DWORD uIconID)成员代表着通知图标的图像句柄。通过不断循环地将该成员置为 0 和有效值。利用人的视觉暂留机理,可以让观众感觉到图标在闪烁。
那么,检测这种状态就很容易了,通过分析 dwMessage 是否是 NIM_MODIFY 并且 uIconID 是否在 0 和非零值之间循环变化即可判断通知图标是否正在闪烁。
一般地,该方法对大多数应用都适用。
通讯软件以及音乐播放器:
截图软件:
由于要适配多种不同的应用环境或者操作系统版本问题,相关代码将在后期补充。
注:获取图标矩形、对应进程的文件路径等信息也将在后期补充。
七、模拟发送消息至托盘图标
(此部分于 2024/08/22 补充)
有一段时间没有在 CSDN 更新了,写这一部分是为了解答读者私信提问的问题。由于窗口子类化的部分我暂时还没有写好,API Hook 也懒得去重新跑挂钩的特征码。所以,就只从技术实现原理的角度去解释吧(只有简单的示例程序)。
首先,我们得了解系统是如何实现通知区域(托盘)中图标的鼠标或键盘响应的。每一个托盘图标在注册时,都需要绑定一个应用程序的窗口。explorer 通过应用程序定义消息标识符 uCallbackMessage 来发送消息到托盘图标绑定的应用程序窗口。根据微软说明,从 Windows 2000 (Shell32.dll 版本 5.0) 开始,如果将 lpdata 指向的 NOTIFYICONDATA 结构的 uVersion 成员设置为 NOTIFYICON_VERSION_4 或更高版本,Shell_NotifyIcon鼠标和键盘事件的处理方式与早期版本的 Windows 不同。
当 uVersion 成员为 0 或 NOTIFYICON_VERSION 时,消息的 wParam 参数包含发生事件的任务栏图标的标识符。 此标识符的长度可以是 32 位。 lParam 参数保存与事件关联的鼠标或键盘消息。 例如,当指针移动到任务栏图标上时, lParam 设置为 WM_MOUSEMOVE。
当 uVersion 成员 NOTIFYICON_VERSION_4 时,应用程序继续通过 uCallbackMessage 成员以应用程序定义消息的形式接收通知事件,但该消息的 lParam 和 wParam 参数的解释将更改如下:
- LOWORD (lParam) 包含通知事件,例如 NIN_BALLOONSHOW、NIN_POPUPOPEN 或 WM_CONTEXTMENU。
- HIWORD (lParam) 包含图标 ID。 图标 ID 的长度限制为 16 位。
- GET_X_LPARAM (wParam) 返回通知事件 NIN_POPUPOPEN、NIN_SELECT、NIN_KEYSELECT 以及 WM_MOUSEFIRST 和 WM_MOUSELAST 之间的所有鼠标消息的 X 定位点坐标。 如果其中任何消息是由键盘生成的, 则 wParam 将设置为目标图标的左上角。 对于所有其他消息, wParam 未定义。
- GET_Y_LPARAM (wParam) 返回为 X 定位点定义的通知事件和消息的 Y 定位点坐标。
- 如果用户使用键盘选择通知图标的快捷菜单,Shell 现在会向关联的应用程序发送 WM_CONTEXTMENU 消息。 早期版本发送 WM_RBUTTONDOWN 和 WM_RBUTTONUP 消息。
- 如果用户使用键盘选择通知图标并使用空格键或 ENTER 键激活它,则 5.0 版 Shell 会向关联的应用程序发送 NIN_KEYSELECT 通知。 早期版本发送 WM_RBUTTONDOWN 和 WM_RBUTTONUP 消息。
- 如果用户使用鼠标选择通知图标并使用 ENTER 键激活它,Shell 现在会向关联的应用程序发送 NIN_SELECT 通知。 早期版本发送 WM_RBUTTONDOWN 和 WM_RBUTTONUP 消息。
假设现在使用 NOTIFYICON_VERSION_4 或更高版本(低版本的处理比较麻烦,暂时不进行分析,可自行去研究),那么当我们知道托盘图标绑定的窗口时,就可以发送模拟消息给消息处理的窗口了,并且程序也应该会正常响应(具体处理是否成功取决于程序是否有模拟消息的检测)。
有人会问 “那你怎么知道托盘图标绑定的消息处理窗口呢?”,其实看懂我文章的读者应该就会知道程序绑定的窗口是 NOTIFYICONDATA 结构的 hWnd (窗口句柄)成员指向的窗口。多个图标可以关联到同一个窗口。通过解析所有的 ICONDATA 信息即可获取窗口句柄,解析的方法可以用过挂钩处理动态获取,也可以通过注册表的转存信息来获取,后者应该相对容易些。
示例程序是微软在 Github 上面公开的托盘消息处理程序的示例,我们对其进行稍微修改(修改窗口尺寸和启用高 DPI 支持)后进行测试。
它通过处理 uCallbackMessage 消息来响应一些操作:
在了解示例程序是如何响应托盘消息后,我们就可以尝试发送模拟消息了,比如下面的打开菜单(一般是右键菜单,如果有的话):
POINT pt = { 0 };
GetCursorPos(&pt);
PostMessageW(hTargetWnd, uCallbackMessage, MAKEWPARAM(pt.x, pt.y), WM_CONTEXTMENU);
对于左键消息,也是通过 LPARAM 传递原始消息的标识符。但我发现这里有一个有趣的现象,就是即使设置了 NOTIFYICON_VERSION_4,也会发送旧版本的消息。比如当左键单击发出时,先发送了 WM_LBUTTONDOWN 再发出 WM_LBUTTONUP ,最后发出一次 NIN_SELECT;左键双击时,先发出 WM_LBUTTONDBLCLK 再发出 WM_LBUTTONUP,最后发出一次 NIN_SELECT。
这样做也是很明显的,因为单靠一个 NIN_SELECT 不足以区分左键的具体操作是单击还是双击。
对于一些细节,我就不再赘述了,可以依照文档的说明自行研究,等我有时间了也会补充相关实现的。
此部分用到的示例程序源代码如下:
链接:https://pan.baidu.com/s/16Wgs8Hm5SqVpCpsJVTqWpA?pwd=iu7j
提取码:iu7j注解:微软原项目代码见:
目前只上传了本小节的项目代码,后期会看情况补充其他小节的项目文件的,也可能会开源到 github。
补充:如果想通过集中发送消息至系统托盘窗口,然后再由系统托盘窗口转发,实现将会比较复杂并且全网暂无更新的研究。不过我可以简单描述这里不同版本系统的窗口的差异:
在 22H2 的 1413 更新至 23H2 的最近更新(推断为 6 月中旬的更新)为止,Win11 系统使用窗口类名为 NotifyIconOverflowWindow 的全新窗口(子窗口为 ToolBar 窗口)来管理溢出区域的托盘图标信息(其他信息仍然由 Shell_TrayWnd 下面的几个窗口管理)。但是新的 23H2 和目前所有的 24H2 均更改为了使用 XAML 窗口(类名为 TopLevelWindowForOverflowXamlIsland 的窗口)的子窗口(标题 DesktopWindowXamlSource,类名 Windows.UI.Composition.DesktopWindowContentBridge)来收发溢出通知区域的消息。
目前能够在窗口激活的状态下模拟鼠标悬停在通知图标上。
测试代码:
#include <iostream>
#include <Windows.h>
// 模拟鼠标移动消息,不改变鼠标实际光标位置
void SimulateMouseMove(int x, int y) {
// 获取当前鼠标位置
POINT currentPos;
GetCursorPos(¤tPos);
// 计算需要发送的鼠标事件
INPUT input = { 0 };
input.type = INPUT_MOUSE;
input.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE;
input.mi.dx = (x * 65535) / GetSystemMetrics(SM_CXSCREEN);
input.mi.dy = (y * 65535) / GetSystemMetrics(SM_CYSCREEN);
// 发送鼠标事件
SendInput(1, &input, sizeof(INPUT));
// 将鼠标恢复到原始位置
SetCursorPos(currentPos.x, currentPos.y);
}
// 模拟后台鼠标点击
void SimulateMouseClick(int x, int y) {
// 获取当前鼠标位置
POINT currentPos;
GetCursorPos(¤tPos);
// 将屏幕坐标转换为绝对值 (0-65535) 范围内的值
int absoluteX = (x * 65535) / GetSystemMetrics(SM_CXSCREEN);
int absoluteY = (y * 65535) / GetSystemMetrics(SM_CYSCREEN);
// 准备输入事件
INPUT inputs[3] = { 0 };
// 设置鼠标移动到目标位置(但不移动光标)
inputs[0].type = INPUT_MOUSE;
inputs[0].mi.dx = absoluteX;
inputs[0].mi.dy = absoluteY;
inputs[0].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE;
// 设置鼠标左键按下事件
inputs[1].type = INPUT_MOUSE;
inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
// 设置鼠标左键释放事件
inputs[2].type = INPUT_MOUSE;
inputs[2].mi.dwFlags = MOUSEEVENTF_LEFTUP;
// 发送输入事件
SendInput(3, inputs, sizeof(INPUT));
// 恢复鼠标位置
SetCursorPos(currentPos.x, currentPos.y);
}
// 打开任务栏溢出通知区域
void OpenOverflowNotificationArea() {
HWND hTrayWnd = FindWindowW(L"Shell_TrayWnd", NULL);
HWND hNotifyWnd = FindWindowExW(hTrayWnd, 0, L"TrayNotifyWnd", L"");
if (hTrayWnd && hNotifyWnd) {
// 找到溢出区域后,模拟鼠标点击事件,打开该区域
RECT rect;
GetWindowRect(hNotifyWnd, &rect);
int x = rect.left + 15; // 当任务栏停靠在屏幕下边缘时,根据通知窗口估计按钮位置
int y = rect.top + 25;
// 模拟鼠标点击,打开溢出区域
SimulateMouseClick(x, y);
std::cout << "Overflow notification area opened.\n";
}
else {
std::cout << "Failed to find overflow notification area.\n";
}
}
int main()
{
// 延时
Sleep(1000);
// 打开并激活溢出通知区域
OpenOverflowNotificationArea();
// 设置目标坐标 (1934, 1251) 或其他托盘图标位置
int targetX = 1900;
int targetY = 1251;
std::cout << "Simulating mouse move to (" << targetX << ", " << targetY << ") without changing actual cursor position...\n";
// 模拟鼠标移动到指定坐标并恢复光标位置
SimulateMouseMove(targetX, targetY);
std::cout << "Mouse move simulated.\n";
std::cout << "Done.\n";
system("pause");
return 0;
}
但是,目前此代码还存在缺陷,主要在激活窗口和发送消息上,目前测试发送 PostMessage 消息没有效果(有人说可能跟通知图标的版本有关系,内部有鼠标位置的检测,但我还没有确认)。
因为速度比较快,所以几乎看不到鼠标光标的位置移动,可以正常显示指定位置的悬停提示,当鼠标光标再次移动时提示才消失。
八、隐藏托盘图标方案(实验)
(此部分于 2024/05/13 补充)
在以前我们可以通过 TB_BUTTON 的 TBDATA 结构设置托盘图标的可见性状态,相关的工具有ShellTrayInfo(Shell Tray Info - CodeProject)。自从微软删除该接口后,我们没有找到替代的直接记录用于设置可见性。但是,我们有一些技巧可以实现 “准隐身” 效果,下面我将简单介绍我的思路。
7.1 截获 WM_COPYDATA 消息(HOOK 途径)
在 explorer 初始化时托管 Shell_TrayWnd 对 WM_COPYDATA 消息的处理,我们截获所有已经注册的通知图标的信息。在需要隐藏图标时候,我们调用 Shell_NotifyIcon 并构造 NOTIFYICONDATA 结构来删除图标。而在要显示某个托盘图标的时候,就重新注册图标。我观察到一款使用 Delphi 语言编写的工具(TrayControl: 点此下载)似乎正在通过接管 WM_COPYDATA 并使用未记录的消息结构来控制图标可见性,虽然实测在最新的 Win11 上无法工作,但是它的内部实现也许是对该任务有帮助的。具体内容有待逆向工程分析。
当然,也有相关讨论提到了该方法的可行性:
7.2 通过注册表隐藏(注册表途径)
Win 11 注册表支持对图标的隐藏和显示,图标信息在注册表有转储(具体运用代码见《最新方案(二)》)。
下面的注册表路径下,有各个图标的显示级别信息,IsPromoted 为 1 表示始终显示通知图标,而为 0 则表示图标隐藏在人字形托盘图标菜单 (System Tray Chevron) 中,猜测该名称由折叠按钮的形状得名。
[HKEY_CURRENT_USER\Control Panel\NotifyIconSettings]
任务栏角溢出通知区域会显示所有放在图标菜单列表中的通知图标。角溢出通知的注册表设置位于路径:
HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify
在该子键下的值项 SystemTrayChevronVisibility 控制图标菜单是否显示。当 SystemTrayChevronVisibility 为 1 时,显示角溢出按钮,当它为 0 时,立即隐藏角溢出通知区域。
该设置和系统的任务栏设置项对应:
其中 “隐藏的图标菜单” 设置对应 SystemTrayChevronVisibility 值项设置,下面的其它系统托盘图标设置开关和 IsPromoted 值项对应。
上面的两个注册表设置会立即生效而不需要重启资源管理器,原因是资源管理器注册了对该路径的监视,当注册表更改时会立即通知资源管理器。
但是微软提供的该方法对用户不友好,甚至我觉得很鸡肋,“隐藏的图标菜单” 会使得在角溢出通知栏里面的图标都被隐藏而无法查看。
此外,我在第一篇中说过,Shell_NotifyIconGetRect 函数能够获取托盘图标的坐标位置和尺寸,它内部使用一个未被记录的 CLSID:GUID_ShellNotifyChevron。
这个名字和注册表的 SystemTrayChevronVisibility 是不是很类似?我相信他们之间一定是有关联的。是否可以编写一个 Shell_NotifyIconSetRect 函数呢?
7.3 通过 XAML Diagnostics 和 IsLands 访问界面元素(Hook Xaml 途径)
在新版的系统上,任务栏已经逐渐转移到 XAML 框架,正在针对 XAML 诊断和 XAML 岛相关功能的利用技术进行实验研究,以便于寻求一个在 Win11 系统大部分新版本上轻松操作界面元素的方法。
九、总结&更新
本文通过微软提供的 SetWindowsHookEx 钩子注入方法,使用 WH_CALLWNDPROC 等转发 Shell_TrayWnd 窗口的 WM_COPYDATA 消息到我们自定义的窗口,并在我们的窗口中对消息进行进一步的处理。此外,还介绍了检测通知图标是否在 “闪烁” 以及如何完全隐藏图标的实验性方案。这是对《最新方案(一)》中的方法做出的补充,如果你有更好的方案,欢迎在系列文章的评论区中交流。
更新清单:
- 实现通过窗口子类化真正拦截或者修改 WM_COPYDATA 消息(重要)
- 实现系统托盘图标信息的可视化,以列表形式呈现结果(重要)
- 实现判断当前通知图标状态的功能(已初步实现)
- 实现对通知图标的隐藏,尝试已知的多种途径(重要)
- 实现一个能够获知图标的位置并支持对图标顺序修改和分组管理的功能(次要)
- 改进挂钩处理的方式,实现进一步获取部分 APP 自定义气泡提示窗口的消息(次要)
以上内容将在以后更新。
【更新日志】
2024.06.01:修正代码中使用 std::cout 导致在早于 Win 10 的系统上运行存在中文乱码问题,改用 C 语言风格的输出,暂未找到 std::cout 乱码原因。
2024.06.06:提出一个新的技术角度(XAML)。
2024.08.22:更新本文内容,增加了对 “模拟消息发送至托盘图标” 使得托盘图标响应鼠标操作的方法,后期将整合代码给出完整方案。
本文属于原创文章,转载请注明出处:
https://blog.csdn.net/qq_59075481/article/details/136240462。
文章发布于:2024.02.22;
更新于:2024.02.22,2024.03.04,2024.05.08/13,2024.06.02 / 06, 2024.07.10,
2024.08.22 / 23 / 27。