經(jīng)常有人問關(guān)于模態(tài)對話框和系統(tǒng)菜單內(nèi)部實現(xiàn)原理方面的問題, 因為系統(tǒng)通過API隱藏了太多細節(jié),這2個問題確實令初學(xué)者甚至是有經(jīng)驗的開發(fā)者困擾, 下面是我個人的一些經(jīng)驗總結(jié)。先說模態(tài)對話框,外部看模態(tài)對話框其實就是Dialog彈出以后函數(shù)(或者說調(diào)用棧call stack)不直接返回, 而是要讓你做出選擇后關(guān)閉Dialog, 然后程序再繼續(xù)往下執(zhí)行。在你關(guān)閉Modal Dialog之前, 你不能做其他操作。下面是我自己模擬模態(tài)對話框行為的代碼:#define MODAL_DLG_EXIT_NOTIFY _T("modal_dialog_can_exit_now")
#define MODAL_DLG_EXIT_VALUE _T("this_is_the_exit_code")
int RunModal(HWND hWnd)
{
int nRet(-1);
HWND hWndOwner = GetWindow(hWnd, GW_OWNER);
BOOL bDisableOwner = FALSE;
if(hWndOwner != GetDesktopWindow())
{
_ASSERT(!(::GetWindowLong(hWndOwner, GWL_STYLE) & WS_CHILD));
EnableWindow(hWndOwner, FALSE);
bDisableOwner = TRUE;
}
MSG msg = {0};
while(GetMessage(&msg, 0, 0, 0))
{
TranslateMessage (&msg);
DispatchMessageW (&msg);
if(GetProp(hWnd, MODAL_DLG_EXIT_NOTIFY) != 0)
{
nRet = (int)GetProp(hWnd, MODAL_DLG_EXIT_VALUE);
break;
}
}
if(bDisableOwner)
{
EnableWindow(hWndOwner, TRUE);
}
DestroyWindow(hWnd);
return nRet;
}
BOOL ExitModal(HWND hWnd, int nExitCode)
{
BOOL bRet = SetProp(hWnd, MODAL_DLG_EXIT_NOTIFY, (HANDLE)1);
SetProp(hWnd, MODAL_DLG_EXIT_VALUE, (HANDLE)nExitCode);
PostMessage(hWnd, WM_NULL, 0, 0);
return bRet;
}
可以看到,其實原理很簡單, 主要就是Disable對話框的Owner窗口, 然后進入消息循壞, 直到你調(diào)用ExitModal (EndDialog) 才退出消息循壞。 現(xiàn)在你也應(yīng)該知道為什么不能用DestroyWindow,而是一定要調(diào)用EndDialog來關(guān)閉模態(tài)對話框的原因了, 因為你直接DestroyWindow就沒有機會Enable它的Owner窗口了。
下面我們再說菜單的實現(xiàn)原理, 相信菜單的原理即使對很多有經(jīng)驗的開發(fā)者也不一定清楚。 我們知道菜單其實也是一個普通的窗口,首先菜單窗口其實和模態(tài)對話框一樣, 在我們關(guān)閉菜單,對菜單做出選擇之前函數(shù)是不會返回的。 菜單窗口的特殊之處在于,菜單彈出的時候我們可以看到它下面的窗口還是保持激活狀態(tài), 也就是說當(dāng)前的得到焦點的窗口其實是菜單的Owner窗口, 但是菜單窗口同時又能響應(yīng)鍵盤消息(我們可以通過上下鍵或是Enter和Esc做出選擇)。從窗口機制的原理上說兩者是矛盾的,一個沒有獲得焦點的窗口怎么能夠響應(yīng)鍵盤消息呢? 下面是我自己對彈出菜單行為的模擬:#define MENU_EXIT_NOTIFY _T("menu_loop_can_exit_now")
#define MENU_EXIT_COMMAND_ID _T("this_is_the_menu_command_id")
int RunMenu(HWND hWnd)
{
int nRet(-1);
BOOL bMenuDestroyed(FALSE);
BOOL bMsgQuit(FALSE);
HWND hWndOwner = GetWindow(hWnd, GW_OWNER);
_ASSERT(GetForegroundWindow() == hWndOwner);
while(TRUE)
{
if(GetProp(hWnd, MENU_EXIT_NOTIFY) != 0)
{
nRet = (int)GetProp(hWnd, MENU_EXIT_COMMAND_ID);
break;
}
if(GetForegroundWindow() != hWndOwner)
{
break;
}
MSG msg = {0};
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if(msg.message == WM_KEYDOWN
|| msg.message == WM_SYSKEYDOWN
|| msg.message == WM_KEYUP
|| msg.message == WM_SYSKEYUP
|| msg.message == WM_CHAR
|| msg.message == WM_IME_CHAR)
{
//transfer the message to menu window
msg.hwnd = hWnd;
}
else if(msg.message == WM_LBUTTONDOWN
|| msg.message == WM_RBUTTONDOWN
|| msg.message == WM_NCLBUTTONDOWN
|| msg.message == WM_NCRBUTTONDOWN)
{
//click on other window
if(msg.hwnd != hWnd)
{
DestroyWindow(hWnd);
bMenuDestroyed = TRUE;
}
}
else if(msg.message == WM_QUIT)
{
bMsgQuit = TRUE;
}
TranslateMessage (&msg);
DispatchMessageW (&msg);
}
else
{
MsgWaitForMultipleObjects (0, 0, 0, 10, QS_ALLINPUT);
}
if(bMenuDestroyed) break;
if(bMsgQuit)
{
PostQuitMessage(msg.wParam);
break;
}
}
if(!bMenuDestroyed) DestroyWindow(hWnd);
return nRet;
}
BOOL ExitMenu(HWND hWnd, int nCommandID = -1)
{
BOOL bRet = SetProp(hWnd, MENU_EXIT_NOTIFY, (HANDLE)1);
SetProp(hWnd, MENU_EXIT_COMMAND_ID, (HANDLE)nCommandID);
return bRet;
}
從代碼可以看到,如果我們可以自己控制整個Windows消息循環(huán),那么中間我們就有很多事可以做了,包括攔截和轉(zhuǎn)發(fā)任何消息,比如我們可以把原來系統(tǒng)發(fā)給A窗口的消息直接轉(zhuǎn)發(fā)給B窗口:菜單窗口的鍵盤消息最初是發(fā)給主窗口的,但是被我們在消息循環(huán)中攔截后,轉(zhuǎn)發(fā)了。簡單總結(jié)下,Windows的API封裝了太多細節(jié), 盡管大部分時候我們只要知道如何使用它們,而不用關(guān)心它們的內(nèi)部如何實現(xiàn)。 但是當(dāng)你寫一些相對底層的東西,比如開發(fā)自己的DirectUI界面庫時, 還是需要真正理解某些API的內(nèi)部實現(xiàn)原理,才能繼續(xù)深入下去。注:因為沒有Windows源碼,上面的代碼只是個人的猜測和模擬,如有不正確的地方歡迎指正。完整測試源碼:ModalDialog&Menu Test
posted on 2013-04-07 22:19
Richard Wei 閱讀(5188)
評論(10) 編輯 收藏 引用 所屬分類:
windows desktop