基於對話框的程序中,每次按下回車鍵時,程序都退出。去掉按鈕的 BS_DEFPUSHBUTTON 屬性並重寫OnOK函數也沒用。那麼如何定製回車鍵的行爲呢?這個問題很easy,但是要說明白,卻要費點時間。
這個問題在Windows的開發中由來已久,對於初學者來說,這是個惱人的問題,幸運的是,人們找到了多種解決這個問題的方案。本文將告訴你定製回車鍵行爲的方法。
如果你想要disable回車鍵,最簡單的方法是重載OnOK函數,這固然是個不壞的主意,但如果你重載OnOK,讓它什麼事情也不幹,那麼當用戶用鼠標按下回車鍵想真正做些什麼的時候怎麼辦呢?你可以改變回車鍵的ID,如:ID_MY_OK,並寫一個調用EndDialog的處理器,這個方法雖然也能行得通,但顯得有點不專業。
另外一種方法是disable回車鍵的“默認”屬性。這也是本文開始所提出的方法,之所以沒有成功,是因爲僅僅uncheck 回車鍵的 BS_DEFPUSHBUTTON 屬性是不夠的,你可以利用Spy++仔細地觀察控制和實驗就能發現回車鍵仍然我行我素髮送退出消息。
問題出在哪呢?你必須區分OK鍵和回車鍵,你可以寫一個OnOK處理器調用GetCurrentMessage函數獲取最後發送的消息,應該是WM_COMMAND,再檢查WPARAM的低位字(low-order word)看看命令來自何處。
要解決問題,必須搞清楚背後所發生的一切,在Spy++中可以看到,當用戶按下回車鍵時,Windows發送一個特殊的WM_GETDEFID消息來獲得缺省的命令ID,Windows再將它作爲WM_COMMAND發送。所以,你要做的就是重載WM_GETDEFID消息,在有關Windows的文檔中是這樣描述WM_GETDEFID返回值的:“如果有缺省得按鈕,則返回值的高位字包含DC_HASDEFID,低位字包含控制的標識符。否則,返回值是零。”
根據這段描述,假設如果沒有缺省得按鈕,則返回值應該是零。如果想要disable缺省得ID,必須在高位字中返回DC_HASDEFID。
BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
ON_MESSAGE(DM_GETDEFID, OnGetDefID)
...
END_MESSAGE_MAP()
LRESULT CMyDlg::OnGetDefID(WPARAM wp, LPARAM lp)
{
return MAKELONG(0,DC_HASDEFID);
}
因爲MFC沒有對應DM_GETDEFID的宏,你必須使用通用的ON_MASSAGE宏。這樣用戶可以隨意按回車鍵,但什麼事都不會發生。上面的做法是解決了按回車鍵程序退出的問題。但是又產生了另外一個問題:如果想要回車鍵做些事情怎麼辦呢?有一些人曾經問過如何將回車鍵映射到TAB鍵,既按下回車鍵就象按下TAB鍵一樣-也就是說輸入焦點移動到下一個對話框控制。這需要做一些工作才行,但最簡單的方式是使用加速鍵。許多程序員試圖用OnChar,我會對他們說:No,no,no! OnChar是一個低級趣味的東西,你應該想方設法避免它,更糟的還有WM_KEYDOWN,WM_KEYUP之類的東西。誰能處理這些東西?OnChar可以用來限制允許輸入編輯框的字符,如:數字,字母等。如果想要將一個鍵映射到一個命令,加速鍵纔是最好的方法。
在本文的例子爲VK_RETURN創建了一個加速鍵,將它映射到命令ID_MY_ENTER,並寫一個命令處理器來做你想做的事情。
BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
ON_COMMAND(ID_MY_ENTER, OnMyEnter)
......
END_MESSAGE_MAP()
void CMyDlg::OnMyEnter()
{
NextInTabOrder();
}
下圖是本文例子的對話框和代碼,代碼中的NextInTabOrder是實際起作用的函數。它使用GetNextDlgTabItem來獲得Tab順序的下一個控制焦點。
如果你細心的話會發現另外一個還沒有得到解決的問題,那就是在MFC對話框不自動處理加速鍵,你必須自己編寫代碼來做這件事情。爲了理解弄清楚這是爲什麼,讓我們回首Windows開發的歷程,在使用C和原始的Windows API的年代,每一個Windows程序中都有一個叫做消息泵的中樞循環:
while (GetMessage(...)) {
TranslateMessage(...);
DispatchMessage(...);
}
在這裏細節不是重要的,重要的是消息並不到達程序的流程,你必須請求消息。這是一種人爲的非搶先式多任務方法,這種方法通過每一個任務精誠協作來仿造多任務環境,隨着增加的功能越來越多,有人想到了加速鍵表的主意,這個表用來映射按鍵和命令IDs。爲了實現這個目的,他們發明了一個叫TranslateAccelerator的函數。現在這個消息泵變成了如下的樣子:
while (GetMessage(...)) {
if (TranslateAccelerator(hAccel...)) {
// handled, continue looping
} else {
TranslateMessage(...);
DispatchMessage(...);
}
}
hAccel是個加速鍵表句柄,在這裏細節同樣不是重要的,重要的是如何利用加速鍵表,也就是要有一個專門的函數將按鍵消息解釋爲WM_COMMAND消息。TranslateAccelerator尋找WM_KEYDOWN,WM_CHAR,WM_KEYUP序列與表中鍵值匹配的字符。如果找到,它插入一條WM_COMMAND到消息隊列,在消息隊列中的命令ID可以是加速鍵表定義的任何入口。這樣你只要設置加速鍵表(在資源中)並記住調用對應的函數TranslateAccelerator,就什麼都不用擔心了。
時間轉眼間進入了21世紀,隨着C++和MFC的日臻成熟,現在幾乎整個消息循環(但不是全部)都被隱藏到了MFC中,爲了能讓任何窗口都有機會獲得一點消息泵的行爲,MFC提供了一個專門的虛函數PreTranslateMessage,如果你有足夠的勇氣去探究CWinThread中的消息處理機制的話,你會遇到類似如下的代碼:
// 簡化後的 CWinThread
while (GetMessage(...)) {
if (PreTranslateMessage(...)) {
// continue looping
} else {
TranslateMessage(...);
DispatchMessage(...);
}
}
CWinThread::PreTranslateMessage是個虛函數,在應用中,其缺省的實現以相同的名字調用另一個虛函數,CWnd::PreTranslateMessage。因此,如果你需要在消息循環中做些什麼的話-如解釋加速鍵-你只要重載PreTranslateMessage即可。實際上,這就是CFrameWnd處理加速鍵的方法。
BOOL CFrameWnd::PreTranslateMessage(MSG* pMsg)
{
......
if (pMsg->message >= WM_KEYFIRST &&
pMsg->message <= WM_KEYLAST)
{
::TranslateAccelerator(m_hAccelTable,...);
}
}
CFrameWnd 從哪裏獲得加速鍵表呢?當你加載框架時,CFrameWnd::LoadFrame用與文檔模板相同的ID(如IDR_MAINFRAME)查找加速鍵表,並將他加載到m_hAccelTable。所有的處理細節在MFC中都是自動的、隱蔽的,你不用去操心-僅對主框架而言,如果是對話框,則是另外一種情況。因爲CDialog不是從CFrameWnd派生而來,所以不繼承任何有關加速鍵的內容。
不用擔心,我們可以模仿CFrameWnd的工作,很容易爲對話框增加加速鍵的功能。第一步是加載加速鍵,加載加速鍵最好的地方是在對話框的OnInitDialog函數中:
BOOL CMyDlg::OnInitDialog()
{
CDialog::OnInitDialog();
......
// Load accelerators
m_hAccel = ::LoadAccelerators(AfxGetResourceHandle(),
m_lpszTemplateName);
ASSERT(m_hAccel);
return TRUE;
}
在加速鍵表中,你可以使用任何ID。這裏我使用的是對話框本身的ID,(m_lpszTemplateName既可以是一個串名,也可以是一個MAKEINTRESOURCE使用的整型ID):
// 本文例子中的加速鍵(In DlgKeys.rc )
IDD_MYDIALOG ACCELERATORS DISCARDABLE
BEGIN
VK_RETURN, ID_MY_ENTER, VIRTKEY, NOINVERT
END
一旦你已經加載加速鍵,剩下的事情是重載PreTranslateMessage函數:
BOOL CMyDlg::PreTranslateMessage(MSG* pMsg)
{
if (WM_KEYFIRST <= pMsg->message &&
pMsg->message <= WM_KEYLAST)
{
HACCEL hAccel = m_hAccel;
if (hAccel &&
::TranslateAccelerator(m_hWnd, hAccel, pMsg))
return TRUE;
}
return CDialog::PreTranslateMessage(pMsg);
}
之所以要檢查按鍵類的消息(從WM_ KEYFIRST 到 WM_KEYLAST)是爲了提高速度。如果你知道不是一個按鍵消息,你就不用浪費時間去調用TranslateAccelerator。再說TranslateAccelerator是一個虛擬函數,不用增加一個消息映射入口。僅僅寫這個函數就可以了。
綜上所述,MFC中爲對話框添加加速鍵功能的方法就是:加載加速鍵和重載PreTranslateMessage函數。也就是說,如果你決定使用加速鍵,不用去操心OnGetDefID,而是將沒有命令處理器的ID映射到VK_RETURN。本文的例子代碼中封裝了一個又加速鍵的新對話框類:CdlgWinAccelerators,它是一個通用類。希望大家喜歡它。最後祝大夥編程愉快。