一种清除windows通知区域“僵尸”图标的方案——问题分析

通知区域名称有趣的历史

        假如说到windows通知区域,可能很多人还是不清楚它是什么。如果改称Tray区域,可能有人就懂了。如果再白话点,叫它“托盘”或者“系统托盘”,可能会有更多的人猜到它是windows什么部位。现在我们揭开它真实的面纱,以windows7系统为例,下图就是它的通知区域。 (转载请指明出于breaksoftware的csdn博客)


        其实,我们叫通知区域为“托盘”或者“系统托盘”是错误的。这个错误并非来源于中文翻译,而是来源于windows发展史上人们对其错误的认识。后来,这个命名也影响了中国一批程序员。我这儿要摘录一个微软老员工的回忆录《The Old New Thing》(中文名《windows编程启示录》)一书中关于这个错误认识起源的一段,还是蛮有意思的。

        “后来,我们将通知图标添加到任务栏中。”

        “我认为人们开始将通知区域叫作系统托盘是因为在Windows95中包含了一个systray.exe的程序,这个程序在通知区域中显示了一些图标,如音量控制,PCMCIA(在当时是叫这个名字)的状态、电池的电量表等。如果你终止了systray.exe,那么这些通知图标也将会消失。因此人们就认为,‘啊,systray程序一定是管理这些图标的组件,我敢打赌这个组件的名字就叫作“系统托盘”’。于是这个误解就形成了,而我们这十几年来一直都在努力澄清这个误解。”

        “更糟糕的是,其他的团队(Shell之外的团队)也错误地使用了这个词,并且开始在他们自己的文档和示例程序里面都使用了系统托盘这个词,其中有一些地方甚至错误地声称系统托盘就是通知区域的正式名称。”

        “有人可能会问,‘你为什么要关心这个名字的正误?既然现在所有的人都叫这个名字,你也可以随波逐流嘛。’”

        “如果每个人都叫错了你的名字,你会乐意吗?”

        其实我觉得,如果微软真的想彻底摒弃“系统托盘”这个名称,最好是从现在做起,将通知区域的一些信息都修改成和Tray这个单词无关。可是,我们使用Spy++查看Windows7任务栏的组成时就会发现,Tray这个单词无处不在啊!

一种清除windows通知区域“僵尸”图标的方案——问题分析_第1张图片


“僵尸图标”

        说了这么多历史故事,我们再回到我们这篇博文要讲述的问题上。其实这个问题,依旧是个历史问题。还好,我发现vista之后的系统上,微软已经意识并修复了这个设计缺陷。我们看下下面的场景

        

        很多使用Windows的人可能都遇到过这个问题:通知区域出现了N个相同的“僵尸”图标。如果我们有意或者无意让光标划过这些图标时,这些图标会悄然消失。我们对这种现象,往往是疑惑一下就抛之脑后。然而,目前我在项目中就接到一个需求:把这些“僵尸”图标自动消失。出于我们产品的设计,我们存在出现这么多“僵尸”图标的场景,于是为了优化用户体验,我需要找到一种方法去解决这种体验问题。


通知区域图标的正常生死过程

        首先要分析一下这个问题出现的原因。一般来说,一个程序在创建时,可能会在通知区域创建一个图标。

一般初始化图标

        创建图标之前,我们需要初始化一个图标

NOTIFYICONDATA m_NotifyIcon;
……
m_NotifyIcon.cbSize = sizeof(m_NotifyIcon);
m_NotifyIcon.uFlags = NIF_ICON | NIF_TIP;
m_NotifyIcon.uVersion = NOTIFYICON_VERSION; // xp
m_NotifyIcon.hWnd = m_hWnd;
m_NotifyIcon.hIcon = m_hIcon;
std::wstring wstrInfo = L"中A英1文"; // 故意取一个晦涩的名字
wmemcpy_s(m_NotifyIcon.szTip, ARRAYSIZE(m_NotifyIcon.szTip), wstrInfo.c_str(), wstrInfo.length()+1 );

       这个地方需要注意的是下面几个参数:

  1. uFlags。我们只是设置了NIF_ICON和NIF_TIP,因为我们需要让我们的通知区域图标变得与众不同,故通过指定这两个标志分别告知系统:我们要设定图标和Tip文字。这个属性我们会在处理Windows7系统上“僵尸”图标的时候再次提起。
  2. hWnd。因为我们图标要相应用户的点击,并将相应消息传递给我们主窗口,所以我们此时要绑定主窗口句柄。这个属性我们会在未来介绍一个特定场景时再次提到。
  3. szTip。我们故意给我们这个图标取了一个晦涩的Tip,这样我们在之后查找“僵尸图标”时将有据可凭。

图标添加到通知区域

        图标初始化后,我们要将图标增加到通知区域
Shell_NotifyIcon(NIM_ADD, &m_NotifyIcon);

        这个图标是可以表明“这个进程还活着”;而且在无界面展现时,让用户方便唤起界面或者执行相应的功能。比如QQ的通知区域图标,它的存在表明QQ进程还是存在的。我们可以左键双击之,可以让主界面展现出来;还可以右击之,可以出现很多快捷功能键

一种清除windows通知区域“僵尸”图标的方案——问题分析_第2张图片

图标从通知区域剔除        

        相应的,如果进程退出,应该通知系统通知区域:要将我设置的通知区域图标删除,因为我马上要退出了。

Shell_NotifyIcon(NIM_DELETE, &m_NotifyIcon);

        如果一切都如此按照规律的“正常生死”,也就没有之前提出的问题。可是,出于策略考虑以及一些异常情况,进程的意外死亡还是不可避免的。这样,如果出现连续的意外死亡场景,系统通知区域就会残留很多“僵尸”图标。为了大战这些“僵尸”,我们需要找到这些“僵尸”的家,然后对“僵尸”各个击破。于是,我们要看下各系统下通知区域的树状结构图。

XP、Win7下通知区域的结构

        先使用SPY++看下XP下任务栏即通知区域的结构

      #32769 (桌面)
        - Shell_TrayWnd
          - Button
          - TrayNotifyWnd
            - TrayClockWClass
            - SysPager
                - ToolbarWindow32(我们关心的,其直接显示在桌面上)
            - Button
              - CiceroUIWndFrame
              - MSTaskSwWClass
                - ToolbarWindow32
          - ReBarWindow32

        SysPager下类名为ToolbarWindow32的控件就是系统通知区域。非常庆幸,XP下只有这么一个通知区域,而且这个通知区域一直是可见的(Win7下有个不可见的通知区域)。

        再看下Win7的通知区域结构

#32769 (桌面)
  - Shell_TrayWnd
    - TrayNotifyWnd
      - TrayClockWClass
      - TrayShowDesktopButtonWClass
      - SysPager
        - ToolbarWindow32(我们关心的,其直接显示在桌面上)
      - ToolbarWindow32(其隐藏在桌面上,通过SendTimeout发送TB_BUTTONCOUNT不能获取其个数)
      - Button
    - ReBarWindow32
      - CiceroUIWndFrame
      - MSTaskSwWClass
        - MSTaskListWClass
        Win7的通知区域相对于XP有点复杂,其中我们一直可见的通知区域的树状结构和XP上是一致的。但是Win7上多出了一个隐藏的通知区域,它和SysPager同级

一种清除windows通知区域“僵尸”图标的方案——问题分析_第3张图片
        针对XP和Win7上都可见的通知区域,我们可以通过如下代码找到相应区域去清理

VOID CKillRunProcessDlg::VisitNotificationArea()
{
    HWND hwndChildAfter = NULL;
    DWORD dwMaxLoopCount = MAXLOOPCOUNT;
    do {
        // 保守性编程,防止死循环
        dwMaxLoopCount--;
        HWND hTrayWnd = NULL;
        hTrayWnd = ::FindWindowEx( NULL, hwndChildAfter, L"Shell_TrayWnd", NULL);
        if ( NULL == hTrayWnd ) {
            break;
        }

        // 找到了窗口类为 Shell_TrayWnd的窗口,
        // 但是不保证找到的就是Notification所在区域的,
        // 所以记录下当前找到的,之后继续找
        hwndChildAfter = hTrayWnd;

        HWND hTrayNotifyWnd = ::FindWindowEx(hTrayWnd, NULL, L"TrayNotifyWnd", NULL );
        if ( NULL == hTrayNotifyWnd ) {
            // 继续找符合条件的Shell_TrayWnd类,然后再在其下找类为TrayNotifyWnd的子窗口
            continue;
        }

        // 这个窗口只能在Win7系统中可以找到
        HWND hToolBar32Ex = ::FindWindowEx( hTrayNotifyWnd, NULL, L"ToolbarWindow32", NULL );
        
        // 在win7 xp下都可以找到该树结构
        HWND hSysPager = ::FindWindowEx( hTrayNotifyWnd, NULL, L"SysPager", NULL );
        HWND hToolBar32Showed = NULL;
        if ( NULL != hSysPager ) {  
            hToolBar32Showed = ::FindWindowEx( hSysPager, NULL, L"ToolbarWindow32", NULL );
        }
        else {
            // 找不到该树结构
            // 则继续找符合条件的Shell_TrayWnd类,然后再在其下找类为TrayNotifyWnd的子窗口
            continue;;
        }

        if ( m_bVistaLater ) {
            if ( NULL == hToolBar32Showed || NULL == hToolBar32Ex ) {
                // 都要有,否则不是合法的
                continue;
            }
        }
        else {
            if ( NULL == hToolBar32Showed ) {
                continue;
            }
        }

        if ( FALSE == IsExplorerProcess(hToolBar32Showed) ) {
            if ( ERROR_NOT_FOUND != ::GetLastError() ) {
                // 不是Explorer进程,则继续寻找
                continue;
            }
        }

        if ( NULL != hToolBar32Showed ) {
            // 清理通知区域
            CleareIcons(hToolBar32Showed);
        }

    } while ( dwMaxLoopCount > 0 );
}

        鉴于XP的通知区域的结构简单性,我决定先从XP系统入手。其实XP上的解决方案是多种的,也是非常有意思的。详细的分析过程可以参看下篇博文《一种清除windows通知区域“僵尸”图标的方案——XP系统解决方案》。

你可能感兴趣的:(一种清除windows通知区域“僵尸”图标的方案——问题分析)