任務欄(Taskbar)是微軟公司在Windows 95中引入的一種特殊的桌面工具條,它為用戶快速訪問計算機資源提供了極大的方便,而狀態欄(以下稱通知欄)無疑是任務欄上較為特殊的一個窗口。編程人員可以調用API函數Shell_NotifyIcon向通知欄發送消息來添加、刪除或修改圖標,當在圖標上發生鼠標或鍵盤事件時,系統會向應用程序發送編程時預先定義的消息,通知欄處理回調函數就會被自動調用以做出相應的處理。實現上述功能的相關文章俯仰即拾,此處不再贅述。本文將討論通知欄編程中幾個較為深入的問題及其在Delphi中的實現方法。
l 新版Windows
操作系統引入的卡通風格的氣泡提示(Balloon ToolT
ips)的實現及相關事件通知
l 外殼Explorer.exe崩潰而重啟后通知欄圖標的自動恢復
l 為通知欄圖標快捷菜單選擇適當的彈出時機
l 鼠標雙擊事件發生時單擊事件的避免
1 氣泡提示(Balloon ToolTips)的實現
1.1 顯示氣泡提示
我們知道,Shell_NotifyIcon函數需要傳入指向某個特定結構的指針,系統根據該結構所包含的信息來決定是向通知欄添加、刪除或修改圖標。該結構的傳統定義如下所示:
_NOTIFYICONDATAA = record | |
| //該結構的大小 |
Wnd: HWND; | //接收通知消息的窗口句柄 |
uID: UINT; | //圖標標識(可以添加多個圖標) |
uFlags: UINT; | //指明該結構中哪些字段的值有效 |
uCallbackMessage: UINT; | //程序定義的接收通知的回調消息 |
hIcon: HICON; | //圖標句柄 |
szTip: array [0..63] of AnsiChar; | //鼠標經過圖標時顯示的提示信息 |
end; | |
氣泡提示(Balloon ToolTips)(如圖1)是裝有Internet Explorer 5及以上版本瀏覽器的操作系統(Windows Me/2000/XP,不包括Windows9x)中引入的通知欄圖標的新行為,同時系統也定義了新版本的NOTIFYICONDATA結構,用于支持氣泡提示。本文中將新結構取名為TNotifyIconData50,其Object Pascal定義及相關字段意義說明如下所示:
TNotifyIconData50 = record | |
前7個字段定義與_NOTIFYICONDATAA基本相同 |
uFlags: UINT; | //uFlags字段增加了如下常數定義 NIF_STATE:dwState、dwStateMask字段有效 NIF_INFO:szInfo、uTimeout、szInfoTitle、 dwInfoFlags字段有效 NIF_GUID:保留值 |
dwState: DWORD; | //圖標狀態 NIS_HIDDEN:圖標是隱藏的 NIS_SHAREDICON:圖標是共享的 |
dwStateMask: DWORD; | //指明dwState的哪些位可以被讀取 如:設置為NIS_HIDDEN則表示圖標的隱藏狀態可以被讀取 |
szInfo: array[0..255] of AnsiChar; | //保存氣泡提示字符串 |
uTimeout: UINT; | //氣泡提示顯示的持續時間 系統默認設置最短10秒,最長30秒 |
szInfoTitle: array[0..63] of AnsiChar; | //保存氣泡提示標題 |
dwInfoFlags: DWORD; | //指明是否在氣泡提示上顯示圖標 NIIF_ERROR:“錯誤”圖標 NIIF_INFO:“信息”圖標 NIIF_NONE:不顯示圖標 NIIF_WARNING:“警告”圖標 NIIF_ICON_MASK:保留值 NIIF_NOSOUND:不播放音效 |
end; | |
| | |
以下代碼演示了在Delphi中如何實現氣泡提示。
//{-------------------常數聲明----------------------
Const
NIIF_NONE = $00000000;
NIIF_INFO = $00000001;
NIIF_WARNING = $00000002;
NIIF_ERROR = $00000003;
//--------------------------------------------------------------------------}
//{------------------類型聲明--------------------
Type
TBalloonTimeout = 10..30; //氣泡提示持續時間,單位為秒
TBalloonIconType = ( //氣泡提示信息圖標控制
bitNone, //不顯示圖標
bitInfo, //“信息”圖標(藍色)
bitWarning, //“警告”圖標(黃色)
bitError); //“錯誤”圖標(紅色)
……
end;
//-----------------------------------------------}
//{---------填寫公共結構----------------------------
PRocedure TEoCSysTray.FillDataStructure;
begin
with FIconData do
begin
cbSize := SizeOf(TNotifyIconData50);
wnd := FWindowHandle;
uID := 0;
uFlags := NIF_MESSAGE or NIF_ICON or NIF_TIP;
//uCallbackMessage、hIcon、szTip三個字段有效
hIcon := FIcon.Handle;
StrPCopy(szTip, FHint);
uCallbackMessage := WM_SYSTRAY;
end;
end; //end of procedure FillDataStructure
//--------------------------------------------------}
//{---------顯示氣泡提示信息----------------------
function TEoCSysTray.Balloon(Title, Text: string;
IconType: TBalloonIconType; Timeout: TBalloonTimeout): Boolean;
const
aBalloonIconTypes : array[TBalloonIconType] of Byte =
(NIIF_NONE, NIIF_INFO, NIIF_WARNING, NIIF_ERROR);
begin
if fActive then //若通知欄圖標處于顯示狀態
begin //刪除原先的氣泡提示
FillDataStructure;
with FIconData do
begin
uFlags := uFlags or NIF_INFO; //設置與氣泡提示相關的字段有效
StrPCopy(szInfo, ''); //設置提示信息為空,刪除氣泡提示
end;
Shell_NotifyIcon(NIM_MODIFY, @FIconData);
//以下顯示新的氣泡提示
FillDataStructure;
with FIconData do
begin
uFlags := uFlags or NIF_INFO;
StrPCopy(szInfo, Text);
uTimeout := Timeout;
StrPCopy(szInfoTitle, Title);
dwInfoFlags := aBalloonIconTypes[IconType];
end {with};
Result := Shell_NotifyIcon(NIM_MODIFY, @FIconData)
end
else
result := True;
end; //end of procedure Balloon
//---------------------------------------------------}
1.2 氣泡提示的事件通知
由于新風格提示的引入,通知欄圖標的消息通知也相應增加,如果通知欄圖標實現了氣泡提示,那么當用戶將鼠標指針移動到通知欄圖標上時,Windows外殼會向通知欄應用程序送出如下四個消息中的一個或多個。
NIN_BALLOONSHOW | 當氣泡提示顯示后外殼發送此消息 |
NIN_BALLOONTIMEOUT | 當氣泡提示由于超時而消失時外殼發送此消息 |
NIN_BALLOONHIDE | 當氣泡提示消失時(比如通知欄圖標被刪除)外殼發送此消息,但氣泡提示由于超時而消失不會產生此消息 |
NIN_BALLOONUSERCLICK | 當用戶點擊鼠標時(點擊氣泡提示和通知欄圖標均可)外殼發送此消息 |
在Delphi強大的消息封裝機制支持下,可以方便地將上述四個消息封裝為四個事件供開發人員使用。簡單來說就是在控件中一個隱藏窗口(創建隱藏窗口的方法可查閱相關文章,此處略過)的窗口消息處理過程中接收這四個消息并分別映射到四個事件,示范代碼如下:
procedure TEoCSysTray.WndProc(var Msg: TMessage);
begin
……
case Msg.LParam of
WM_LBUTTONDOWN:
……
WM_RBUTTONDBLCLk:
……
else if Msg.lParam = NIN_BALLOONSHOW then //氣泡提示顯示后
begin
if Assigned(FOnBalloonShow) then
FOnBalloonShow(Self)
end
else if Msg.lParam = NIN_BALLOONHIDE then //氣泡提示由于超時而消失
begin
if Assigned(FOnBalloonHide) then
FOnBalloonHide(Self)
end
else if Msg.lParam = NIN_BALLOONTIMEOUT then //氣泡提示消失
begin
if Assigned(FOnBalloonTimeOut) then
FOnBalloonTimeOut(Self)
end
else if Msg.lParam = NIN_BALLOONUSERCLICK then //用戶點擊鼠標
begin
if Assigned(FOnBalloonClick) then
FOnBalloonClick(Self)
end
else
Msg.Result := DefWindowProc(FWindowHandle, Msg.Msg, Msg.wParam, Msg.lParam);
end;
end; //end of procedure WndProc
2 Windows發生錯誤導致外殼Explorer重啟時圖標的重建
相信很多Windows用戶都碰到過這種情況:運行某個程序時出現意外錯誤,導致外殼程序Explorer.exe崩潰而發生重啟(即Explorer.exe被關閉后重新運行),任務欄也在消失后重新生成,但應用程序在通知欄添加的圖標消失了,雖然這些程序仍在運行,但再也無法通過通知欄圖標與用戶交互。為避免這種情況出現,Windows提供了相應的機制。
在安裝了Internet Explorer 4.0及以上版本的Windows操作系統中,當任務欄建立后,外殼會向所有頂層的應用程序發出通知消息,該消息是外殼以字符串“TaskbarCreated”為參數向系統注冊獲得的,應用程序窗口接收到該消息后就應該重新添加的通知欄圖標。
在Delphi中實現過程如下:
initialization
MsgTaskbarRestart := RegisterWindowMessage(‘TaskbarCreated’);
1. 重載主窗口的消息處理過程,攔截任務欄重建消息,進行重新添加圖標的操作。
procedure TMainForm.WndProc(var Message: TMessage);
begin
……
if Message.Msg = MsgTaskbarRestart then
begin
TrayIcon.Active := False; //刪除通知欄圖標
TrayIcon.Active := True; //添加通知欄圖標
end;
……
inherited WndProc(Message);
end; //end of WndProc
值得一提的是,如果將自動恢復的功能封裝為控件,將以后的開發帶來方便。但由于外殼只向所有頂層的應用程序發送通知,封裝起來有一定的困難。因為通知欄圖標的回調函數只能接收WM_XBUTTONDOWN、WM_XBUTTONUP等有限的幾個消息,并不能接收所有的窗口消息。
解決的方法是使用SetWindowLong函數。通過向它傳入GWL_WNDPROC參數,可以改變一個窗口的窗口過程。只需在創建控件時將應用程序窗口的窗口過程指針保存起來,并指向為控件中的某個新的窗口處理過程,在控件中就能夠響應所有的窗口消息了(包括任務欄重建的消息);當控件銷毀的時候再將保存的原始窗口過程指針恢復即可,此處不再贅述。
3 與通知欄圖標關聯的快捷菜單彈出的時機
本節將討論編寫通知欄應用程序時應該注意的一個問題,即快捷菜單彈出的時機問題。Windows為通知欄圖標提供了幾個鼠標消息(事件),那么我們應該將彈出快捷菜單的代碼寫在哪個事件中呢?先別急于回答“放在MouseDown事件中”,事實上,這個看似簡單的問題,其中卻小有講究。許多軟件(有的甚至號稱專業級軟件)也都或多或少忽視了這個問題。
首先需要明確一個軟件設計中通用的原則,即:應當給用戶一個機會以確認是否執行他選擇的操作。這在軟件設計中有很多例子。大的方面,最普遍的,如用戶選擇了刪除文件,應彈出窗口予以確認。小的方面,如Windows中對鼠標的常規處理,也有一個確認的動作。一般來說,Windows中的程序對于鼠標事件的響應都是這樣:在用戶松開鼠標后才認為他確認了點擊操作。以按鈕(Button)為例,對于Windows的標準按鈕,用戶都可以在按下鼠標后而未松開鼠標前把鼠標移動到按鈕區域以外來取消這次單擊操作。再如Windows中窗口系統菜單的彈出,當用戶在窗口標題欄上按下鼠標右鍵后,可以把鼠標移動到標題欄以外再松開,這樣系統菜單就不會彈出,即等價于用戶取消了該次操作。
遵照這個原則,通知欄快捷菜單的彈出顯然應該在用戶松開鼠標按鍵后,即WM_XBUTTONUP消息到來時才發生,以保證用戶能夠在松開鼠標之前取消其彈出,而不應簡單的把彈出菜單的代碼放在WM_XBUTTONDOWN的消息響應中。縱觀Windows操作系統附帶的程序,皆是如此。
4 鼠標雙擊事件發生時單擊事件的避免
編寫過通知欄應用程序的朋友大概都碰到過這樣的情況:如果編寫了響應鼠標單擊(WM_XBUTTONUP)與雙擊(WM_XBUTTONDBLCLK)的代碼,那么在用戶雙擊鼠標時單擊事件也會發生。而在實際應用中通常希望單擊與雙擊是相互獨立的兩個操作,它們之間不應該互相影響。對于這一問題,有些軟件采用“鴕鳥戰術”,不響應單擊事件(即對WM_XBUTTONUP消息不作響應),只響應雙擊事件,這未嘗不是一種解決辦法,但浪費了單擊事件,算不得好。通過下面的分析,我們將會看到一個較為令人滿意的解決方法。
4.1 原理分析
在Windows中并沒有定義表示鼠標單擊的消息,單擊事件在Delphi等可視化編程語言中定義為鼠標按下后松開,因而單擊事件一般在WM_XBUTTONUP中觸發。而雙擊事件則不同,它在Windows中有明確的定義,當用戶雙擊任意一個鼠標按鍵時,實際上按如下順序Windows送出了四次消息:WM_XBUTTONDOWN、WM_XBUTTONUP、WM_XBUTTONDBLCLK、WM_XBUTTONUP。顯然,如果響應WM_XBUTTONUP消息而觸發了單擊事件,那么雙擊時必然會先觸發一次單擊。
我們的目的是對雙擊事件單獨處理,為此只需引入一個延時機制即可。讓計時器在發生WM_XBUTTONDOWN時開始計時,待超時后檢查WM_XBUTTONDBLCLK是否已經發生,若已發生則觸發雙擊事件,否則觸發單擊事件。關鍵的是延時多久才合適呢?長了沒有意義,短了可能超時后WM_XBUTTONDBLCLK都沒有發生。顯然應該至少延遲雙擊時兩次單擊之間的時間間隔,這一時間可以有系統API函數GetDoubleClickTime得到。
4.2 解決方案
按照如下幾個步驟對通知欄圖標控件的代碼稍加修改即可(注意WM_XBUTTONUP等消息中的“X”可為“L”、“M”、“B”,表示鼠標左鍵、中鍵、右鍵)。
A. 定義兩個變量FMouseDblClicked和FMouseUp,分別用以指示雙擊和鼠標松開是否已經發生,均初始化為False。
B. 再為TEoCTrayIcon控件添加一個TTimer類成員變量FTimer,并在OnCreate事件中對它進行初始化:
constructor TEoCSysTray.Create(AOwner: TComponent);
begin
……
FMouseDblClicked := False;
FMouseUp := False;
FTimer := TTimer.Create(Self);
with FTimer do
begin
Enabled := False;
Interval := GetDoubleClickTime; //時鐘間隔設為雙擊的時間間隔。
OnTimer := OnButtonTimer; //設置時鐘超時響應過程。
end;
……
end; //end of Create
C. 接下來在前述重載的隱藏窗口消息處理過程中響應不同消息來設置上述兩個變量的狀態。
procedure TEoCSysTray.WndProc(var Msg: TMessage);
begin
……
case Msg.LParam of
WM_XBUTTONDOWN:
begin
……
FMouseDblClicked := False;//雙擊尚未發生
FMouseUp := False; //鼠標尚未松開
FTimer.Enabled := False; //結束上次延時
FTimer.Enabled := True; //開始延時
end;
WM_XBUTTONUP:
FMouseUp := True; //設置鼠標已經松開,便于Timer檢查
WM_XBUTTONDBLCLk:
begin
FMouseDblClicked := True; //設置雙擊已經發生的標志
觸發雙擊事件;
end;
else
Msg.Result := DefWindowProc(FWindowHandle, Msg.Msg, Msg.wParam, Msg.lParam);
end;
end; //end of WndProc
D. 在延時處理程序中判斷鼠標狀態,觸發單擊事件。
procedure TEoCSysTray.OnButtonTimer(Sender: TObject);
begin
FTimer.Enabled := False;
if (not FMouseDblClicked) and FMouseUp then //雙擊尚未發生且鼠標已松開
begin
觸發單擊事件;
觸發MouseUp事件;
end;
end; //end of procedure OnButtonTimer
如此一來,單擊事件就表現為WM_XBUTTONDOWN, Click, WM_XBUTTONUP,而雙擊事件則表現為WM_XBUTTONDOWN, WM_XBUTTONDBLCLK(過濾掉了兩條MW_XBUTTONUP消息),從而避免了雙擊事件發生時觸發單擊事件。
5 總結
關于通知欄圖標的編程還有很多話題,比如動態切換圖標、響應MouseLeave和MouseEnter事件等,在實際中都有應用,難以面面俱到。