通知區(qū)域名稱有趣的歷史
假如說到windows通知區(qū)域,可能很多人還是不清楚它是什么。如果改稱Tray區(qū)域,可能有人就懂了。如果再白話點,叫它“托盤”或者“系統(tǒng)托盤”,可能會有更多的人猜到它是windows什么部位?,F(xiàn)在我們揭開它真實的面紗,以windows7系統(tǒng)為例,下圖就是它的通知區(qū)域。
(轉載請指明出于breaksoftware的csdn博客)

其實,我們叫通知區(qū)域為“托盤”或者“系統(tǒng)托盤”是錯誤的。這個錯誤并非來源于中文翻譯,而是來源于windows發(fā)展史上人們對其錯誤的認識。后來,這個命名也影響了中國一批程序員。我這兒要摘錄一個微軟老員工的回憶錄《The Old New Thing》(中文名《windows編程啟示錄》)一書中關于這個錯誤認識起源的一段,還是蠻有意思的。
“后來,我們將通知圖標添加到任務欄中。”
“我認為人們開始將通知區(qū)域叫作系統(tǒng)托盤是因為在Windows95中包含了一個systray.exe的程序,這個程序在通知區(qū)域中顯示了一些圖標,如音量控制,PCMCIA(在當時是叫這個名字)的狀態(tài)、電池的電量表等。如果你終止了systray.exe,那么這些通知圖標也將會消失。因此人們就認為,‘啊,systray程序一定是管理這些圖標的組件,我敢打賭這個組件的名字就叫作“系統(tǒng)托盤”’。于是這個誤解就形成了,而我們這十幾年來一直都在努力澄清這個誤解。”
“更糟糕的是,其他的團隊(Shell之外的團隊)也錯誤地使用了這個詞,并且開始在他們自己的文檔和示例程序里面都使用了系統(tǒng)托盤這個詞,其中有一些地方甚至錯誤地聲稱系統(tǒng)托盤就是通知區(qū)域的正式名稱?!?/p>
“有人可能會問,‘你為什么要關心這個名字的正誤?既然現(xiàn)在所有的人都叫這個名字,你也可以隨波逐流嘛?!?/p>
“如果每個人都叫錯了你的名字,你會樂意嗎?”
其實我覺得,如果微軟真的想徹底摒棄“系統(tǒng)托盤”這個名稱,最好是從現(xiàn)在做起,將通知區(qū)域的一些信息都修改成和Tray這個單詞無關。可是,我們使用Spy++查看Windows7任務欄的組成時就會發(fā)現(xiàn),Tray這個單詞無處不在??!

“僵尸圖標”
說了這么多歷史故事,我們再回到我們這篇博文要講述的問題上。其實這個問題,依舊是個歷史問題。還好,我發(fā)現(xiàn)vista之后的系統(tǒng)上,微軟已經(jīng)意識并修復了這個設計缺陷。我們看下下面的場景

很多使用Windows的人可能都遇到過這個問題:通知區(qū)域出現(xiàn)了N個相同的“僵尸”圖標。如果我們有意或者無意讓光標劃過這些圖標時,這些圖標會悄然消失。我們對這種現(xiàn)象,往往是疑惑一下就拋之腦后。然而,目前我在項目中就接到一個需求:把這些“僵尸”圖標自動消失。出于我們產(chǎn)品的設計,我們存在出現(xiàn)這么多“僵尸”圖標的場景,于是為了優(yōu)化用戶體驗,我需要找到一種方法去解決這種體驗問題。
通知區(qū)域圖標的正常生死過程
首先要分析一下這個問題出現(xiàn)的原因。一般來說,一個程序在創(chuàng)建時,可能會在通知區(qū)域創(chuàng)建一個圖標。
一般初始化圖標
創(chuàng)建圖標之前,我們需要初始化一個圖標
- 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 );
這個地方需要注意的是下面幾個參數(shù):
- uFlags。我們只是設置了NIF_ICON和NIF_TIP,因為我們需要讓我們的通知區(qū)域圖標變得與眾不同,故通過指定這兩個標志分別告知系統(tǒng):我們要設定圖標和Tip文字。這個屬性我們會在處理Windows7系統(tǒng)上“僵尸”圖標的時候再次提起。
- hWnd。因為我們圖標要相應用戶的點擊,并將相應消息傳遞給我們主窗口,所以我們此時要綁定主窗口句柄。這個屬性我們會在未來介紹一個特定場景時再次提到。
- szTip。我們故意給我們這個圖標取了一個晦澀的Tip,這樣我們在之后查找“僵尸圖標”時將有據(jù)可憑。
圖標添加到通知區(qū)域
圖標初始化后,我們要將圖標增加到通知區(qū)域
- Shell_NotifyIcon(NIM_ADD, &m_NotifyIcon);
這個圖標是可以表明“這個進程還活著”;而且在無界面展現(xiàn)時,讓用戶方便喚起界面或者執(zhí)行相應的功能。比如QQ的通知區(qū)域圖標,它的存在表明QQ進程還是存在的。我們可以左鍵雙擊之,可以讓主界面展現(xiàn)出來;還可以右擊之,可以出現(xiàn)很多快捷功能鍵

圖標從通知區(qū)域剔除
相應的,如果進程退出,應該通知系統(tǒng)通知區(qū)域:要將我設置的通知區(qū)域圖標刪除,因為我馬上要退出了。
- Shell_NotifyIcon(NIM_DELETE, &m_NotifyIcon);
如果一切都如此按照規(guī)律的“正常生死”,也就沒有之前提出的問題??墒?,出于策略考慮以及一些異常情況,進程的意外死亡還是不可避免的。這樣,如果出現(xiàn)連續(xù)的意外死亡場景,系統(tǒng)通知區(qū)域就會殘留很多“僵尸”圖標。為了大戰(zhàn)這些“僵尸”,我們需要找到這些“僵尸”的家,然后對“僵尸”各個擊破。于是,我們要看下各系統(tǒng)下通知區(qū)域的樹狀結構圖。
XP、Win7下通知區(qū)域的結構
先使用SPY++看下XP下任務欄即通知區(qū)域的結構
- #32769 (桌面)
- - Shell_TrayWnd
- - Button
- - TrayNotifyWnd
- - TrayClockWClass
- - SysPager
- - ToolbarWindow32(我們關心的,其直接顯示在桌面上)
- - Button
- - CiceroUIWndFrame
- - MSTaskSwWClass
- - ToolbarWindow32
- - ReBarWindow32
SysPager下類名為ToolbarWindow32的控件就是系統(tǒng)通知區(qū)域。非常慶幸,XP下只有這么一個通知區(qū)域,而且這個通知區(qū)域一直是可見的(Win7下有個不可見的通知區(qū)域)。
再看下Win7的通知區(qū)域結構
- #32769 (桌面)
- - Shell_TrayWnd
- - TrayNotifyWnd
- - TrayClockWClass
- - TrayShowDesktopButtonWClass
- - SysPager
- - ToolbarWindow32(我們關心的,其直接顯示在桌面上)
- - ToolbarWindow32(其隱藏在桌面上,通過SendTimeout發(fā)送TB_BUTTONCOUNT不能獲取其個數(shù))
- - Button
- - ReBarWindow32
- - CiceroUIWndFrame
- - MSTaskSwWClass
- - MSTaskListWClass
Win7的通知區(qū)域相對于XP有點復雜,其中我們一直可見的通知區(qū)域的樹狀結構和XP上是一致的。但是Win7上多出了一個隱藏的通知區(qū)域,它和SysPager同級

針對XP和Win7上都可見的通知區(qū)域,我們可以通過如下代碼找到相應區(qū)域去清理
- VOID CKillRunProcessDlg::VisitNotificationArea()
- {
- HWND hwndChildAfter = NULL;
- DWORD dwMaxLoopCount = MAXLOOPCOUNT;
- do {
- // 保守性編程,防止死循環(huán)
- dwMaxLoopCount--;
- HWND hTrayWnd = NULL;
- hTrayWnd = ::FindWindowEx( NULL, hwndChildAfter, L"Shell_TrayWnd", NULL);
- if ( NULL == hTrayWnd ) {
- break;
- }
-
- // 找到了窗口類為 Shell_TrayWnd的窗口,
- // 但是不保證找到的就是Notification所在區(qū)域的,
- // 所以記錄下當前找到的,之后繼續(xù)找
- hwndChildAfter = hTrayWnd;
-
- HWND hTrayNotifyWnd = ::FindWindowEx(hTrayWnd, NULL, L"TrayNotifyWnd", NULL );
- if ( NULL == hTrayNotifyWnd ) {
- // 繼續(xù)找符合條件的Shell_TrayWnd類,然后再在其下找類為TrayNotifyWnd的子窗口
- continue;
- }
-
- // 這個窗口只能在Win7系統(tǒng)中可以找到
- 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 {
- // 找不到該樹結構
- // 則繼續(xù)找符合條件的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進程,則繼續(xù)尋找
- continue;
- }
- }
-
- if ( NULL != hToolBar32Showed ) {
- // 清理通知區(qū)域
- CleareIcons(hToolBar32Showed);
- }
-
- } while ( dwMaxLoopCount > 0 );
- }
鑒于XP的通知區(qū)域的結構簡單性,我決定先從XP系統(tǒng)入手。其實XP上的解決方案是多種的,也是非常有意思的。詳細的分析過程可以參看下篇博文《一種清除windows通知區(qū)域“僵尸”圖標的方案——XP系統(tǒng)解決方案》。