自從Windows 95面市以來,系統托盤應用作為一種極具吸引力的UI深受廣大用戶的喜愛。使用系統托盤UI的Windows應用程序數不勝數,比如"金山詞霸"、"Winamp"、"RealPlayer"等等。那麼如何編寫自己的托盤應用呢?本文是系列文章中的第一篇,這些文章將比較系統地描述托盤應用的編程。並創建自己的C++類來增強系統托盤應用的特性。讀完這些文章,再參照例子,相信讀者能輕松自如地在自己的程序中應用系統托盤。
大家知道,MFC框架沒有提供任何現成的類應用於系統托盤UI,那麼如何將表示應用程序的圖標添加到任務欄中呢?方法很簡單,只用到一個API函數,它就是Shell_NotifyIcon。這個函數本身也相當容易理解和使用。看看它的原型就知道了:
BOOL Shell_NotifyIcon(
DWORD dwMessage,
PNOTIFYICONDATA pnid
);
第一個參數dwMessage類型為DWORD,表示要進行的動作,它可以是下面的值之一:
NIM_ADD: 添加一個圖標到任務欄。
NIM_MODIFY: 修改狀態欄區域的圖標。
NIM_DELETE: 刪除狀態欄區域的圖標。
NIM_SETFOCUS: 將焦點返回到任務欄通知區域。當完成用戶界面操作時,任務欄圖標必須用此消息。例如,如果任務欄圖標正
顯示上下文菜單,但用戶按下"ESCAPE"鍵取消操作,這時就必須用此消息將焦點返回到任務欄通知區域。
NIM_SETVERSION:指示任務欄按照相應的動態庫版本工作。
第二個參數pnid是NOTIFYICONDATA結構的地址,其內容視dwMessage的值而定。這個結構在SHELLAPI.H文件中定義如下:
typedef struct _NOTIFYICONDATA {
DWORD cbSize; // 結構大小(sizeof struct),必須設置
HWND hWnd; // 發送通知消息的窗口句柄
UINT uID; // 圖標ID ( 由回調函數的WPARAM 指定)
UINT uFlags;
UINT uCallbackMessage; // 消息被發送到此窗口過程
HICON hIcon; // 圖標句柄
CHAR szTip[64]; // 提示文本
} NOTIFYICONDATA;
uFlags的值:
#define NIF_MESSAGE 0x1 // 表示uCallbackMessage 有效
#define NIF_ICON 0x2 // 表示hIcon 有效
#define NIF_TIP 0x4 // 表示szTip 有效
有關Shell_NotifyIcon函數的詳細使用細節請參考MSDN。
NOTIFYICONDATA結構中的 hWnd 是"擁有" 圖標的窗口句柄。uID可以是任何標示托盤圖標的ID(如果有多個圖標),一般使用資源ID。HIcon可以是任何圖標的句柄,包括預定義的系統圖標,如IDI_HAND、IDI_QUESTION、IDI_EXCLAMATION、或者Windows的徽標IDI_WINLOGO。
圖標的顯示並不難,關鍵是事件的處理。 當用戶將鼠標移到圖標上或者在圖標上單擊鼠標時,為了得到通知消息,你可以將自己的消息ID賦給uCallbackMessage,並設置NIF_MESSAGE標志。當用戶在圖標上移動或單擊鼠標時,Windows將用hWnd指定的窗口句柄調用你建立的窗口過程;消息ID在uCallbackMessage中指定,uID的值即為wParam,lParam為鼠標事件,如WM_LBUTTONDOWN等。
盡管Shell_NotifyIcon函數簡單實用。但它畢竟是個Win32 API,為此我將它封裝在了一個C++類中,這個類叫做CTrayIcon,有了它,托盤編程會更加輕松自如,因為它隱藏了NOTIFYICONDATA、消息代碼、標志以及所有那些你必須要看MSDN才能搞掂的繁瑣細節。CTrayIcon的定義以及實現細節請下載源代碼參考。CTrayIcon為程序員提供了一個更加友好的托盤編程接口,它除了對Shell_NotifyIcon函數進行打包之外,它還是一個迷你框架呢!之所以這麼說,是因為按照Windows系統應用軟件界面指南所提倡的原則(這個指南可以在MSDN中找到),這個類增強了托盤圖標的用戶界面行為。以下便是CTrayIcon最終實現的UI特性:
1、 托盤圖標應該有信息提示,也就是ToolTips。
2、 單擊右鍵應該彈出上下文菜單,這個菜單中應包含打開屬性頁的命令或者打開與圖標相關的其它窗口的命令。
3、 單擊左鍵應該顯示進一步的信息或者控制圖標所代表的對象,例如,當左鍵單擊聲音圖標時進行音量控制。如果沒有進一步的信息或控制,則不要有任何動作。
CTrayIcon對上面的特性進行了全面的封裝。為了示范CTrayIcon的工作原理,本文提供一個例子程序TrayTest1,圖一是運行程序後顯示的一個對話框:
圖一 TrayTest1運行後顯示的對話框
當把圖標安裝到系統托盤之後,如果雙擊托盤圖標,程序會彈出一個消息列表窗口,只要你的鼠標在托盤圖標上移動或點擊(無論是左右鍵的單擊或雙擊),產生的消息都會顯示在這個窗口裡,如圖二:
圖二 消息顯示窗口
當鼠標光標移到托盤圖標上時,在圖標附近會顯示提示信息,如圖三:
圖三 顯示Tooltip
為了正確使用CTrayIcon,首先你必須在程序的某個地方實例化CTrayIcon,例子程序是在主框架中創建CTrayIcon實例的。
Class MainFrame public CFrameWnd {protected: CTrayIcon m_trayIcon; // my tray icon
…….
};
然後,你必須提供一個ID。這是在圖標生命期內的唯一標示,即便以後你修改了要顯示的圖標。這個ID也是鼠標事件發生時你將獲得的ID。它不一定必須是圖標的資源ID,例子程序中這個ID為IDR_TRAYICON,由框架的構造函數CMainFrame通過成員初始化列表對m_trayIcon進行初始化:
CMainFrame::CMainFrame() : m_trayIcon(IDR_TRAYICON){
……
}
為了添加圖標,必須根據具體情況調用下列的 SetIcon 函數之一:
m_trayIcon.SetIcon(IDI_MYICON); //資源 ID
m_trayIcon.SetIcon("myicon"); //資源名
m_trayIcon.SetIcon(hicon); //HICON
m_trayIcon.SetStandardIcon(IDI_WINLOGO); //系統圖標
除了SetIcon(UINT uID)之外,這些函數都有一個LPCSTR類型的可選參數用於指定提示文本。SetIcon(UINT uID)使用ID與uID相同的串資源作為提示文本。例如,TrayTest1有一行代碼是這樣的:
// (在mainframe.cpp文件中)
m_trayIcon.SetIcon(IDI_MYICON);
這行代碼也設置了提示信息,因為TrayTest1有一個串資源,其ID也是IDI_MYICON。這在TRAYTEST.RC文件中可以看到:
STRINGTABLE PRELOAD DISCARDABLE
BEGIN
IDI_MYICON "雙擊圖標激活 TRAYTEST."
END
如果你想改變圖標,可以用不同的ID或者HICON再次調用SetIcon函數之一。CTrayTest便會用NIM_MODIFY而不是NIM_ADD來改變圖標。相同的函數甚至可以用於刪除圖標,如:
m_trayIcon.SetIcon(0); //刪除圖標
CTrayIcon將此代碼解釋成NIM_DELETE。你已經看到,所有這些表示行為的編碼,標志都被一個使用方便的函數所替代:這都歸功於C++!現在,我們來看看如何處理通知消息以及前面提到的所有UI特性。通知消息的處理必須要設置圖標之前,但是要在創建窗口之後調用CTrayIcon::SetNotificationWnd,做這件事情的最佳場所是在OnCreate處理例程中,TrayTest就是在這裡處理的:
// 注冊用於托盤的自定義消息
#define WM_MY_TRAY_NOTIFICATION WM_USER+0
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
……
// 請通知我
m_trayIcon.SetNotificationWnd(this,
WM_MY_TRAY_NOTIFICATION);
m_trayIcon.SetIcon(IDI_MYICON);
return 0;
}
消息一旦注冊,接下來你便可以用通常的消息映射方式處理托盤通知消息。
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_MESSAGE(WM_MY_TRAY_NOTIFICATION,
OnTrayNotification)
// (or ON_REGISTERED_MESSAGE)
END_MESSAGE_MAP()
LRESULT
CMainFrame::OnTrayNotification(WPARAM wp, LPARAM lp)
{
……
// 顯示消息
……
return m_trayIcon.OnTrayNotification(wp, lp);
}
當消息處理器得到控制,WPARAM的值是在構造CTrayIcon時指定的ID;LPARAM為鼠標事件(如WM_LBUTTONDOWN)。當你得到通知消息後,可以做任何想做的的事情;例子程序TrayTest此時是顯示通知信息,細節請參考源代碼。完成消息的處理之後,調用CTrayIcon::OnTrayNotification進行缺省處理。此虛擬函數(所以你可以改寫)實現我前面提到過的缺省的UI行為。尤其是處理WM_LBUTTONDBLCLK和WM_RBUTTONUP。CTrayIcon尋找與圖標ID相同的某個菜單(如IDR_TRAYICON),如果找到,則當用戶右鍵單擊圖標時CTrayIcon顯示這個菜單;當用戶數雙擊圖標時,CTrayIcon執行第一個菜單命令。只有兩件事情需要進一步交待:
第一件事情是:在顯示菜單之前,CTrayIcon讓第一個菜單項為默認,所以它以黑體顯示。但如何用黑體來顯示某個菜單項呢?我在\MSDEV\INCLUDE\*.H搜索了一番,發現了Get/SetMenuDefaultItem。這個函數沒有相關的CMenu打包類,所以我必須直接調用它們。
// 讓第一個菜單項為默認(黑體):
::SetMenuDefaultItem(pSubMenu->m_hMenu, 0, TRUE);
這裡0表示第一個菜單項,TRUE說明用位置表示菜單項的ID。為什麼MFC沒有打包Get/SetMenuDefaultItem函數呢?微軟的家伙們解釋那是因為這些函數(其它的還有::Get/SetMenuItemInfo, ::LoadImage等)還沒有在最新的Windows版本中實現。一旦在最新的Windows版本中實現了,便會馬上添加到MFC中。
第二件事情是上下文菜單的顯示:
::SetForegroundWindow(m_nid.hWnd); ::TrackPopupMenu(pSubMenu->m_hMenu, ...);
為了讓TrackPopupMenu在托盤的上下文中正確運行,你必須首先調用SetForegroundWindow,否則,當用戶按下ESCAPE鍵或者在菜單之外單擊鼠標時,菜單不會消失。為解決這個問題,我花費了數個小時,最後還是在MSDN上找到了解決方法。為了解詳情,請參考MSDN的Q135788。最讓我哭笑不得的是我花了那麼多時間來關注這個問題,最後微軟的這幫家伙在MSDN上給你來了一個問題的結論是:“This behavior is by design.....”真是氣剎人也。
正如你所看到的,CTrayIcon使得托盤應用的編程變得易如反掌。TrayTest1要做的事情不外乎調用CTrayIcon::OnTrayNotification實現一個通知消息處理器,提供一個與圖標ID相同的菜單。就這麼簡單。
// (TRAYTEST.RC文件)
IDR_TRAYICON MENU DISCARDABLE
BEGIN
POPUP "托盤(&T)"
BEGIN
MENUITEM "打開(&O)", ID_APP_OPEN
MENUITEM "關於 TrayTest(&A)...", ID_APP_ABOUT
MENUITEM SEPARATOR
MENUITEM "退出TrayTest 程序(&S)", ID_APP_SUSPEND
END
END
當用戶在托盤圖標上單擊右鍵,CTrayIcon顯示這個菜單,如圖四所示。如果用戶雙擊圖標,CTrayIcon執行第一個菜單命令:“打開”,此時激活TrayTest(正常狀態下是隱藏的)。為了終止TrayTest1,你必須選擇"Suspend TRAYTEST"菜單項。如果你從“文件|退出”退出,或者關閉TrayTest1主窗口,TrayTest1不會真正關閉,它只是將自己隱藏起來。這個行為是TrayTest1改寫了CMainframe::OnClose實現的。
圖四 TRAYTEST1 托盤圖標菜單
最後,我想說明一個很讓人擔心的問題,每個人在看到這個小圖標後都想盡快的在自己的程序中加入托盤圖標。作為程序員,這完全是可以理解的。當自己的程序中成功添加了托盤圖標,在朋友們中間炫耀一番,那種感覺確實很好。但是要記住:並不是所有的應用都需要用托盤圖標,如果不是必須就不要畫蛇添足,否則托盤圖標太多必然造成屏幕垃圾,看看下面圖五吧:
圖五 托盤圖標程序“噩夢版”
看到這麼多的托盤圖標對於用戶來說簡直就是噩夢。(待續)
本文配套源碼