我正在做一個歷時很久的項目。出於某些原因,項目啟動之初我們實現了自己的彈出式菜單。當工具提示信息出現之後,我們將這個功能引入了我們自己的菜單,以便當用戶將鼠標停留在某個菜單項上的時候,能夠出現相應的提示信息。這一功能對於我們的用戶來說非常重要,因為 用它可以解釋為什麼某個菜單項是被禁用的。由於我們的用戶對 Windows 平台越來越熟悉,他們想要外觀上更標准的菜單。現在我們使用了 CMenu,但是我們失去了 出色的菜單提示信息。請問如何在 MFC 中實現菜單提示信息呢?
Joakim Fagerli
多美妙的想法。Figure 1 的效果勝過千言萬語。他展示了一個我寫的菜單提示信息小程序——MenuTips,它實現了任何 MFC 應用程序均可復用的菜單提示信息。具備菜單提示信息特性真的很棒,因為它又排除了一個狀態欄存在的理由。即便沒有狀態欄,你依然能夠知曉每一個命令是做什麼用的。更重要的是,提示信息 顯示在每個菜單項旁邊很更顯眼。在當今的巨型顯示器面前,很多用戶甚至根本就意識不到出現在狀態條上的菜單提示信息——它離人們的視線太遠了。
Figure 1 菜單提示信息
我在類 CMenuTipManager 裡面實現了菜單提示。如果你想在自己的應用程序中使用菜單提示功能,只需要在主窗口類中添加一個 CMenuTipManager對象,然後在 創建框架的時候調用 Install 即可:
//in CMainFrame::OnCreate(...)
m_menuTipManager.Install(this);
需要做的就這麼多。現在當用戶將鼠標停留在某個菜單項上面超過一秒鐘,菜單提示信息管理器就會將對應的命令提示顯示成一條提示信息,如 Figure 1 所示。CMenuTipManager 從你的程序的串表中獲取提示信息,那也是 MFC 尋找狀態欄提示信息的地方。
CMenuTipManager 使用了我聞名於世的子類化窗口類 CSubClassWnd 來捕獲發往主窗口的 WM_MENUSELECT 消息。當用戶在主菜單、系統菜單甚至上下文菜單中選中不同的的菜單項時,Windows 都會像 宿主窗口發送一個 WM_MENUSELECT 消息。如果你想提供反饋信息或者做其它自己你想做的事,此時便是最佳時機。MFC 的 CFrameWnd::OnMenuSelect 處理 WM_MENUSELECT 消息以便在狀態欄上顯示命令提示信息。CMenuTipManager 捕獲同樣的消息來顯示菜單提示信息,Figure 2 展示了相關的代碼。
總體上來說,CMenuTipManager 還是非常容易理解的,但是在 Windows 中還是有幾點需要注意。首先是工具提示信息本身:有人曾指出過如何使用 Windows 標准的工具提示信息麼?我在 2000 年 9 月和 2001 年 6 月的專欄中使用的是 CPopupText 類。CPopupText 非常簡單,甚至一個知道如何敲分號的 VB 專家都能夠實現它。 你只需要實例化一個 CPopupText 對象,調用 Create 和 SetWindowText,然後 CPopupText::ShowDelayed 就會在指定的時間裡顯示提示信息了。CPopupText::Cancel 負責刪除提示信息。唯一的難點是使 CPopupText 看起來和標准的工具提示信息一樣。為了實現這一目的,CPopupText 使用了菜單字體並且調用 GetSystemColor(COLOR_INFOBK) 得到包含工具提示顏色的系統顏色。 具體細節請參考本文附帶的源代碼。
對於 CMenuTipManager 而言,最復雜的部分是如何放置提示信息,以便恰好與高亮菜單項的右面對齊。這個問題基本思路是先得到菜單的位置,然後進行一系列的算術運算將所有的菜單項高度加起來,直到達到了被選中的菜單項。但是怎樣才能得到菜單的位置呢?這可不是一個簡單的問題。你也許猜到了,菜單本身也是一個窗口,但是沒有 API 可以用來得到它的句柄,那怎麼辦呢?我曾經多次提到,在 Windows 中總會有解決辦法,你決不會被困住的。
CMenuTipManager 有一個靜態的輔助函數 CMenuTipManager::GetRunningMenuWnd,它返回當前正在運行的菜單窗口。鑒於這個函數的使用頻率非常高,我將其設定為公有。但這個函數是如何工作的呢?你也許考慮調用 WindowFromPoint 來得到位於鼠標下面的窗口。多數情況下這種方法能夠達到目的,但是不要忽略一種情況:用戶可能會通過鍵盤而非鼠標來調用菜單,此種情形下光標可能位於任何位置,而未必是在菜單上的。所以 CMenuTipManager 改為調用 ::EnumWindows 列舉出所有頂層窗口,並且在其中尋找一個使用了特殊類名 #32768(Windows 為菜單窗口使用的類名)的窗口。 static BOOL MyEnumProc(HWND hwnd, LPARAM lParam)
因為只會顯示一個菜單,所以 MyEnumProc 函數找到的第一個就恰恰是我們需要的。即便由於某些非常古怪的原因,有兩個菜單同時出現,EnumWindows 也會按照z軸上自頂向下的順序列舉窗口,所以第一個被找到的菜單窗口也一定就是當前的活動菜單了。很聰明的做法不是麼?一旦你找到了菜單窗口(HWND 或者 CWnd),剩下的就只是為提示信息的出現位置進行一些像素運算了。Figure 2 中的 CMenuTipManager::OnMenuSelect 展示了細節工作。
{
char buf[16];
GetClassName(hwnd, buf, sizeof(buf));
if (strcmp(buf,"#32768")==0) { // menu window
// save hwnd
return FALSE; // no need to look further
}
return TRUE; // keep looking
}
那麼提示信息文本怎麼樣呢?CMenuTipManager 提供了另外一個輔助函數, CMenuTipManager::GetMessageString,用以得到與每一個菜單命令相關聯的提示信息字符串。這個函數是我或多或少地從 CFrameWnd::GetMessageString 直接拷貝過來的。為什麼要復制這個函數?這樣一來你就可以在沒有主框架的情況下調用它了。CFrameWnd::GetMessageString 應該是靜態的,但是不知道哪 位友好的微軟員工在編寫這個函數時顯然沒有注意到根本不需要 CFrameWnd。為什麼在加載字符串資源的時候一定需要通過主框架窗口?為了通用性,我 創建自己的靜態版本函數。
當我開始實現用戶從菜單項上移開鼠標光標,提示信息必須消失的功能時,我遇到了另外一個非常奇怪的問題。對於主窗口而言,如果用戶將鼠標指針移出菜單時,Windows 發送一個 WM_MENUSELECT 消息 ,並且在消息中附帶有父菜單句柄和一個 MF_POPUP 標志,這樣一來就有可能知道所發生的事情從而隱藏提示信息。但是對於上下文菜單來說,就沒有那麼幸運了。當用戶將鼠標移出上下文菜單時,並沒有 WM_MENUSELECT 消息通知你。
沒關系,在 Windows 中總會有解決方法。這種情況下,Windows 發送了一個不同的消息WM_ENTERIDLE。事實上,當程序等待輸入並且對話框或者菜單被顯示的時候,Windows 都會發送 WM_ENTERIDLE 消息。Windows 甚至通情達理到同時傳遞了對話框或者菜單的窗口句柄 HWND,吃驚吧?所以你所需要做的就是在接受到 WM_ENTERIDLE 消息的時候拿這個窗口句柄與鼠標下的窗口句柄進行比較。如果鼠標下的窗口句柄與隨 WM_ENTERIDLE 發送過來的相同,那麼鼠標仍然停留在菜單上面;如果鼠標下的是其 它窗口的句柄,那麼說明用戶已將鼠標移出上下文菜單,取消提示信息的時機到了。Figure 2 中的 CMenuTipManager::OnEnterIdle 函數完成的就是這個功能。
最後,CMenuTipManager 使用了一個 m_bSticky 標記來控制提示信息是立即出現還是延遲一段時間之後才出現。當用戶第一次使用某個菜單項的時候,菜單提示信息的出現是需要等待一段時間的。但是如果已經出現過一次提示信息,那麼用戶在選擇其 它的新菜單項時就不必再等。所以一旦顯示過提示信息,CMenuTipManager 就將 m_bSticky 置為 TRUE,以便隨後的提示信息能夠立即顯示出來。取消菜單或者調用其 它命令將 m_bSticky 重新設置為 FALSE。
無論菜單項是處於啟用還是禁用狀態,CMenuTipManager 都會顯示同樣的提示信息。如果想在你的程序中顯示為什麼一個菜單項被禁用的信息,你就必須對 CMenuTipManager 和 MFC 的相關機制做一些改動。MFC 期待命令字串具備“長提示信息\n短提示信息”的格式,那意味著 MFC 總是預期有一個分隔長提示信息和短提示信息的換行符。MFC 將長提示信息顯示在狀態欄上、將短提示信息顯示在工具欄上。你應該對這種處理方式進行擴展以便能夠加入為什麼菜單被禁 用的解釋字符串。你必須將 CMenuTipManager::GetResCommandPrompt 函數改寫為能夠接受兩個參數,以便適應命令提示信息為(long/short/disable)的格式,並且你需要改寫 OnGetCommandPrompt 函數以便在菜單項有MF_DISABLED 標志時得到菜單項禁 用的提示信息。我將這部分工作留給讀者作為練習。