一种清除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这个单词无处不在啊!



“僵尸图标”

        说了这么多历史故事,我们再回到我们这篇博文要讲述的问题上。其实这个问题,依旧是个历史问题。还好,我发现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进程还是存在的。我们可以左键双击之,可以让主界面展现出来;还可以右击之,可以出现很多快捷功能键


图标从通知区域剔除        

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

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同级


        针对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系统解决方案》

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
此文件是MyFll.fll文件不可分割的一部分,包含对MyFll函数库全部说明及示例。 MyFll是专为Microsoft Visual FoxPro设计的扩展库,部分函数来自任明汉(RMH myF1论坛)前辈提供的myDll代码翻译而成,由于很多功能利用VFP处理比较烦锁,或无法实现,使用此库可有效解决VFP的弱项。部分与Win32Api同名的函数,是Win32API的封装。部分函数取自网上开源算法,在相关函数中均有注明。 此源代码使用VC6 sp6开发,采用VFP9 Sp2的库文件,理论上可以应用于VFP6 7 8 9版本。 此库在第一次打开时自动添加智能感应功能已关闭。请在打开后,使用FllAddFoxCode()向智能感应库添加所有的函数原型(需要VFP7以上),此库存所有函数使用VFP推荐的命名规则,一般可以根据函数名和知能感应的提示参数来推测参数及返回值。 开发环境可以使用FllHelp()函数来检查函数信息: 取得此版本函数个数:FllHelp() 取得第x个函数的信息:?FllHelp(x) 显示提示信息:?FllHelp(x,.T.) 显示指定函数的信息:FllHelp("SendMessage",.T.) 显示Fll的版:?FllGetVersion() 添加智能感应代码:FllAddFoxCode() 版本信息:由于不定期更新,请及时核对版本(右键属性),版本为X.X.X.X,第一位为主版本号,第二位为函数个数,第三位为发布的年份,第四位为日期。当前版本为:1.179.9.811 版权:你可以自由使用、散发此函数库及此帮助,包括应用于你的商业软件中。在转发时应注意帮助文件于函数库和帮助同在。不得对软件进行破解、反编译等破性或逆向工程。MyFll作者不承担可能由于技术原因或失误给你带来的错误或损失。发现错误可以与作者取得联系共同改进。 感谢:我的帮助论坛http://www.myf1.net/bbs 梅子论坛 http://www.meizvfp.com/bbs 感谢:各位为Myfll做测试、编写帮助的热心网友。 作者:木瓜 [email protected] 函数列表: 硬件相关函数: GetDiskSerial        读取指定硬盘的序列号 GetCpuId           读取CPU的序列号 GetMAC            读取网卡的MAC地址 GetVolumeNumber       读取指写磁盘的卷标 IsDiskInDrive        检查指定磁盘是否就绪 GetPort           读取系统的串口、并口 ComOpen           打开串口 ComWrite           向串口输出信息 ComRead            从串口读取信息 ComClose           关闭串口 GetGUID            获取全球唯一ID AEthernet          枚举网卡的所有信息 加密解密函数: MD5File           计算一个文件的MD5效验和 MD5String          计算一个字符串的MD5效验和 CRC32File          计算一个文件的CRC32效验和 CRC32String         计算一个字符串的CRC32效验和 des             采用DES算法加密或解密一个字符串,长度为8位 des16            采用DES算法加密或解密一个字符串,长度为16位 des24            采用DES算法加密或解密一个字符串,长度为24位 EnDeString          双向加密解密字符串的函数 Encrypt           双向加密解密字符串 RSACalc           RSA计算函数 RSAGen            生成随机RSA密钥函数 RSACmp            比较两个十六进制值是否相等 URLEncode          URL编码 URLDecode          URL解码 QPEncode           Quoted-Printable QP编码 QPDecode           Quoted-Printable QP解码 压缩解压函数: Zip              压缩文件 UnZip             解压文件 UnZipFile           解压单个文件           ZipInfo            测试一个文件是否在压缩文件中存在 ZipFileToStr         将压缩文件中的文件解压到变量 ZipAFile           将zip文件中的文件信息生成的数组 CompressString        压缩一个字符串 DeCompressString       解压字符串 数据库: CursorToStr         将Cursor生成变量 ChangesToStr         将Cursor的变动情况生成变量         StrToCursor         将变量还原为Cursor AppendFromStr        将变量中的表追加到指定表中 ACursorList         将变量中的表信息生成数组 ACursorFields        将变量中的指定表的字段信息生成数组 CRC32Record         计算一条记录的CRC32值 SQLCallBack         SQL回调函数载入 SQLCallReset         SQL回调函数卸载 PackMDB           压缩修复Access数据库 ReadMemo           读取vfp中大于16M的备注字段 网络相关函数: DownFile           从internet上下载文件到本地 DownFileX           线程方式从网上下载一个文件 HttpGetFileSize       读取internet上的文件大小 HttpPostData         向http服务器Post数据 HttpOpen           打开一个Internet句柄 HttpAddParms         为Http句柄添加一个参数 HttpSend           发送Http句柄中的Post数据 HttpClose          关闭Http句柄 FtpDownFile          从FTP服务器下载文件 FtpUploadFile        向FTP服务器上传文件 FtpConnect          连接FTP服务器 FtpDisconnect        断开FTP服务器连接 FtpAFile          枚举Ftp中的所有文件 FtpCreateDir        在FTP服务器上创建一个文件夹         FtpSetCurrentDir      设置当前的文件夹 FtpDeleteDir        删除FTP服务器上的文件夹 FtpDelFile         删除FTP服务器上的文件 FtpRename          重命名FTP服务器上的文件 FtpFOpen          打开FTP服务器上的文件 FtpFClose         关闭FTP服务器上的文件 FtpFRead          读取FTP服务器上的文件 FtpFWrite          写入FTP服务器上的文件 FtpSetPasv          设置FTP被动工作模式 ShareAdd           在局域网共享文件夹 ShareDel           删除共享文件夹的共享 ShareMapDrive         映射网络驱动器 ShareDelDrive         删除映射的网络驱动器 GetLocalIP          读取本机IP DomainToIP          转换域名为IP地址 DialUp            建立拨号连接 DialDown           断开拨号连接 GetSqlServer         列出所有网络上的SQL Server IpToMAC           根据IP得到MAC地址 邮件函数: SmtpCreate          创建一个SMTP发送邮件的句柄 SmtpNewMail         在内存中创建邮件内容,等待发送 SmtpSend           发送邮件 SmtpGetLastError       检测邮件最后的错误 Pop3Create          创建一个POP3接收邮件的句柄 Pop3Close          关闭句柄 Pop3AMailList        枚举POP3服务器上的邮件数量 Pop3DeleteMail        删除POP3邮件服务器上的邮件 Pop3GetMail         下载一封邮件 Pop3GetMailHeader      下载邮件头 Pop3DeleteMail        删除服务器上的邮件 打印相关函数: PaperAdd           添加自定义纸张并返回ID PaperDel           删除自定义纸张 PaperInfo          读取所有纸张信息或指定纸张信息 APaper            枚举所有纸张生成数组 GetDefaultPrinter      取得默认打印机的名称 SetDefaultPrinter      设置默认打印机 PrinterOpen         启用一个打印任务 PrinterOutPage        打印页对像 PrinterClose         结束打印 常用转换函数: ToPY             生成指写字符串的拼音首字 NToC             数字转人民币大写 hzbh             计算指定字符串的汉字笔划 StringToDword        将高底位存放的字符串,转换为字符型 DwordToString        将整型数值转换为字符型数值 NumConver          进制转换函数,能够将一个数字转换为2至36进制的字符串 ConverNum           将指定进制的字符串转换为数值型 GB2312ToBIG5         简体转繁体 BIG5ToGB2312         繁体转简体 FormToBmp          将表单保存为BMP图片 RectToBmp           指定屏幕区域保存为bmp图片 StrReverse          指定屏幕区域保存为bmp图片 ImageConver         转换图片格式 FTrim            删除字符串中除指定字符以外的字符 Thumbnail          缩放图片 注册表读写函数: regRead           读取注册表中的设置 regWrite           向注册表中写入设置 regDelKey          删除注册表中的分支 regDelValue         删除注册表中的设置 ini文件读写函数: iniRead           从ini文件中读取设置 iniWrite           向ini文件中写入一个设置 iniSet            设置默认的数据段和ini文件 iniSetSection        设置默认的数据段 iniSetFileName        设置默认的ini文件 系统托盘 SysTrayAdd          向系统托盘添加一个图标 SysTrayEdit         修改系统托盘的图标 SysTrayDel          删除系统托盘中的图标 HotKeyAdd          向系统注册一个热键 HotKeyDel          删除向系统注册的热键 SysTrayShowMessage      显示托盘消息 文件操作 CopyFiles          复制文件 MoveFiles          移动文件 DeleteFiles         删除文件 CreateShortcut        创建快捷方式 GetIcon           释放exe或dll中的图标 其它: SetVFPEvents         设置MYFLL内部函数回调功能 SetDateTime         设置Windows系统时间 ChangEres          更改屏幕的分辨率 idleLoad           开始加载空闲检测 idleSeconds         读取系统空闲时间 idleUnload          卸载空闲检测 KillApp           终止指定进程 KillAllApp          终止所有进程,但排除指定标题的进程 KillProcessByName      根据程序文件名杀死一个进程 FindAllFile         查找指定文件夹下的所有文件 IsNum            检测函数是否是数字 CheckProcess         检测主程序是否重复运行 ProcLoad            载入一个VFP函数,取得函数地址 ProcUnload          卸载函数 MemRead           读取指定地址的内存 MemWrite           写入指写内存 PushError          系统错误处理压栈 PopError           系统错误处理出栈 Format            格式化输出字符串(类C语言) CreateLink          创建文件关联 DesktopHide         隐藏桌面 SystemKeySet         禁用系统键 RegisterFile         注册文件名解除文件注册 封装过的Win32 API函数: FindWindow          查找指定窗口的句柄 SendMessage         向指写窗口发送指定消息 PostMessage         将消息投递到指定句柄的消息队列 ShowWindow          显示指定窗口 ShowWindowAsync       显示指定窗口 SetWindowLong        设置窗口的扩展样式 SetLayeredWindowAttributes  设置窗口效果 SetForegroundWindow     激活指定窗口使这成为最前面的窗口 ShellExecute         调用系统关联,打开一个文件 ShellExecWait        调用一个程序,并等待执行结束 SuspendThread        暂停一个线程 ResumeThread         恢复一个线程 Beep             使电脑内喇叭发出beep声 ExitWindowsEx        退出Windows Sleep            程序挂起nMilliseconds毫秒 ExitProcess         终止当前进程 GetLastError         返回最后的错误号码 SetParent          设置窗口的父窗口 UpdateWindow         更新窗口

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

breaksoftware

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

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

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

打赏作者

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

抵扣说明:

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

余额充值