经常有人问关于模态对话框和系统菜单内部实现原理方面的问题, 因为系统通过API隐藏了太多细节,这2个问题确实令初学者甚至是有经验的开发者困扰, 下面是我个人的一些经验总结。

先说模态对话框,外部看模态对话框其实就是Dialog弹出以后函数(或者说调用栈call stack)不直接返回, 而是要让你做出选择后关闭Dialog, 然后程序再继续往下执行。在你关闭Modal Dialog之前, 你不能做其他操作。
下面是我自己模拟模态对话框行为的代码:
#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窗口, 然后进入消息循坏, 直到你调用ExitModal (EndDialog) 才退出消息循坏。 现在你也应该知道为什么不能用DestroyWindow,而是一定要调用EndDialog来关闭模态对话框的原因了, 因为你直接DestroyWindow就没有机会Enable它的Owner窗口了。

下面我们再说菜单的实现原理, 相信菜单的原理即使对很多有经验的开发者也不一定清楚。
我们知道菜单其实也是一个普通的窗口,首先菜单窗口其实和模态对话框一样, 在我们关闭菜单,对菜单做出选择之前函数是不会返回的。 菜单窗口的特殊之处在于,菜单弹出的时候我们可以看到它下面的窗口还是保持激活状态, 也就是说当前的得到焦点的窗口其实是菜单的Owner窗口, 但是菜单窗口同时又能响应键盘消息(我们可以通过上下键或是Enter和Esc做出选择)。从窗口机制的原理上说两者是矛盾的,一个没有获得焦点的窗口怎么能够响应键盘消息呢? 下面是我自己对弹出菜单行为的模拟:
#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消息循环,那么中间我们就有很多事可以做了,包括拦截和转发任何消息,比如我们可以把原来系统发给A窗口的消息直接转发给B窗口:菜单窗口的键盘消息最初是发给主窗口的,但是被我们在消息循环中拦截后,转发了。

简单总结下,Windows的API封装了太多细节, 尽管大部分时候我们只要知道如何使用它们,而不用关心它们的内部如何实现。 但是当你写一些相对底层的东西,比如开发自己的DirectUI界面库时, 还是需要真正理解某些API的内部实现原理,才能继续深入下去。

注:因为没有Windows源码,上面的代码只是个人的猜测和模拟,如有不正确的地方欢迎指正。

完整测试源码:ModalDialog&Menu Test
posted on 2013-04-07 22:19 Richard Wei 阅读(5075) 评论(10)  编辑 收藏 引用 所属分类: windows desktop

FeedBack:
# re: DirectUI中模态对话框和菜单的原理
2013-04-07 23:23 | waiting4you
这个问题确实值得探讨,菜单弹出时父窗体貌似没有焦点了吧?
我的做法是菜单模拟窗体打开时用SetCapture取得控制权,一旦窗体收到WM_CAPTURECHANGED消息就把窗体退出。  回复  更多评论
  
# re: DirectUI中模态对话框和菜单的原理
2013-04-08 08:37 | Richard Wei
@waiting4you
SetCapture的方法应该是不正确定。
菜单弹出时焦点当然还在父窗口上,这是菜单窗口是没有获得焦点的, 菜单窗口的键盘消息需要父窗口转发给他。我的测试代码基本上已经完整的模拟了菜单的行为。
  回复  更多评论
  
# re: DirectUI中模态对话框和菜单的原理
2013-04-08 09:35 | waiting4you
@Richard Wei
你那个方法更灵活,只是自己处理消息的话有时会影响到其它地方的代码,如OnIdle之类的。
用SetCapture的方式做的话,用SW_SHOWNA的方式显示,父窗体还是处于Active状态但没有输入焦点,优点是这种方法实现比较简单。  回复  更多评论
  
# re: DirectUI中模态对话框和菜单的原理
2013-04-08 09:58 | Richard Wei
@waiting4you

对, OnIdle如果是放在Timer里是没有影响的 ,如果是自己在在消息循环里调用, 就只能写成全局函数,然后在这里调用了。

Windows系统对于菜单的实现应该不是用SetCapture的方式, 调用TrackPopupMenu后也是阻塞式的,至于父窗口有保持没有激活可以看标题栏是不是保持高亮。  回复  更多评论
  
# re: DirectUI中模态对话框和菜单的原理[未登录]
2013-04-09 10:03 | jacky
模态对话框还有许多东西要模拟的吧,比如点击父窗口时的闪烁效果  回复  更多评论
  
# re: DirectUI中模态对话框和菜单的原理
2013-04-09 10:47 | Richard Wei
@jacky
这是系统系统的默认行为。
当然我上面的是简化版的实现, 实际情况要考虑更多问题, 具体可以参考MFC里DoModal的实现源码  回复  更多评论
  
# re: DirectUI中模态对话框和菜单的原理[未登录]
2013-04-14 13:01 | 春秋十二月
好久没搞windows了 看完后 受益了 兄弟功底扎实  回复  更多评论
  
# re: DirectUI中模态对话框和菜单的原理
2013-04-14 19:10 | Richard Wei
@春秋十二月
受益就好, 其实关于菜单的原理我也是想了好久才明白的 ^_^  回复  更多评论
  
# re: DirectUI中模态对话框和菜单的原理
2013-09-05 16:03 | Richard Wei
记录下, 对于菜单窗口, 可以优化消息WM_MOUSEACTIVATE, 返回MA_NOACTIVATE
  回复  更多评论
  
# re: DirectUI中模态对话框和菜单的原理
2013-10-30 23:49 | 华锋
对话框应该有相应的 API 做处理

DialogProc,
DialogBox,
EndDialog,
....

  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理