获取 Windows 系统托盘图标信息的最新方案(三)

目录

前言

一、原理解释

二、实现 CallWndProcHook

三、安装钩子例程

四、创建消息处理窗口

五、完整代码和注意事项

六、判断指定图标是否正在 “闪烁”

七、隐藏托盘图标方案(实验)

7.1 截获 WM_COPYDATA 消息(HOOK 途径)

7.2 通过注册表隐藏(注册表途径)

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 23H2 以及测试渠道的 24H2。(2024.03.04)

(2)近期,我准备修改本文。有很多读者问我关于图标状态的问题,这也属于本文的范畴,只不过之前因为时间原因没有讲解清楚。(2024.05.04)


系列文章列表:

编号文章标题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 编码的。

Shell_NotifyIconA 内部将字符集隐式转换为宽字符

结构的第一个成员是一个哨兵值(签名),因为 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 替换为窗口子类化(未来考虑实现)或者窗口过程挂钩(第一篇讲过)!!!

二、实现 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 上运行效果:

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 参数功能
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 和非零值之间循环变化即可判断通知图标是否正在闪烁。

检测结果的示例 1

一般地,该方法对大多数应用都适用。

通讯软件以及音乐播放器:

检测结果的示例 2

截图软件:

检测结果的示例 3

由于要适配多种不同的应用环境或者操作系统版本问题,相关代码将在后期补充。

注:获取图标矩形、对应进程的文件路径等信息也将在后期补充。

七、隐藏托盘图标方案(实验)

(此部分于 2024/05/13 补充)

在以前我们可以通过 TB_BUTTON 的 TBDATA 结构设置托盘图标的可见性状态,相关的工具有ShellTrayInfo(Shell Tray Info - CodeProject)。自从微软删除该接口后,我们没有找到替代的直接记录用于设置可见性。但是,我们有一些技巧可以实现 “准隐身” 效果,下面我将简单介绍我的思路。

ShellTrayInfo 工具利用 TBDATA 结构的图标控制实现

7.1 截获 WM_COPYDATA 消息(HOOK 途径)

在 explorer 初始化时托管 Shell_TrayWnd 对 WM_COPYDATA 消息的处理,我们截获所有已经注册的通知图标的信息。在需要隐藏图标时候,我们调用 Shell_NotifyIcon 并构造 NOTIFYICONDATA 结构来删除图标。而在要显示某个托盘图标的时候,就重新注册图标。我观察到一款使用 Delphi 语言编写的工具(TrayControl: 点此下载)似乎正在通过接管 WM_COPYDATA 并使用未记录的消息结构来控制图标可见性,虽然实测在最新的 Win11 上无法工作,但是它的内部实现也许是对该任务有帮助的。具体内容有待逆向工程分析。

Tray Control - HOOK 模块的 HookProc 函数

当然,也有相关讨论提到了该方法的可行性:

Stackoverflow 上的一个话题

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 时,立即隐藏角溢出通知区域。 

托盘图标菜单是否显示的注册表设置

该设置和系统的任务栏设置项对应:

托盘图标设置的 GUI 界面

其中 “隐藏的图标菜单” 设置对应 SystemTrayChevronVisibility 值项设置,下面的其它系统托盘图标设置开关和 IsPromoted 值项对应。

上面的两个注册表设置会立即生效而不需要重启资源管理器,原因是资源管理器注册了对该路径的监视,当注册表更改时会立即通知资源管理器。

但是微软提供的该方法对用户不友好,甚至我觉得很鸡肋,“隐藏的图标菜单” 会使得在角溢出通知栏里面的图标都被隐藏而无法查看。

此外,我在第一篇中说过,Shell_NotifyIconGetRect 函数能够获取托盘图标的坐标位置和尺寸,它内部使用一个未被记录的 CLSID:GUID_ShellNotifyChevron

Shell_NotifyIconGetRect 函数主要伪代码

这个名字和注册表的 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:提出一个新的技术角度。


本文属于原创文章,转载请注明出处:

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。

  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
可以使用Windows API中的Shell_NotifyIconGetRect函数来查询系统托盘中的图标数量。 具体步骤如下: 1. 枚举系统托盘中的所有图标获取每个图标的位置信息。 2. 使用Shell_NotifyIconGetRect函数获取托盘区域的大小和位置。 3. 遍历每个图标的位置信息,如果该图标的位置在托盘区域内,则将计数器加一。 示例代码如下: ```c++ #include <windows.h> #include <shellapi.h> int GetTrayIconCount() { int count = 0; HWND trayWnd = FindWindow("Shell_TrayWnd", NULL); if (trayWnd == NULL) { return count; } HWND trayNotifyWnd = FindWindowEx(trayWnd, NULL, "TrayNotifyWnd", NULL); if (trayNotifyWnd == NULL) { return count; } RECT trayRect; Shell_NotifyIconGetRect(&GUID_NULL, &trayRect); HWND childWnd = FindWindowEx(trayNotifyWnd, NULL, "SysPager", NULL); if (childWnd != NULL) { childWnd = FindWindowEx(childWnd, NULL, "ToolbarWindow32", NULL); } else { childWnd = FindWindowEx(trayNotifyWnd, NULL, "ToolbarWindow32", NULL); } if (childWnd == NULL) { return count; } int buttonCount = SendMessage(childWnd, TB_BUTTONCOUNT, 0, 0); for (int i = 0; i < buttonCount; i++) { RECT buttonRect; SendMessage(childWnd, TB_GETITEMRECT, i, (LPARAM)&buttonRect); if (IntersectRect(&buttonRect, &buttonRect, &trayRect)) { count++; } } return count; } ``` 该函数首先获取系统托盘窗口的句柄,然后通过FindWindowEx函数获取托盘区域的句柄。接着使用Shell_NotifyIconGetRect函数获取托盘区域的大小和位置,并通过FindWindowEx函数获取托盘区域中的ToolbarWindow32控件。最后,遍历ToolbarWindow32控件中的所有按钮,并判断按钮的位置是否在托盘区域内,如果是,则将计数器加一。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涟幽516

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值