MFC中实现模态对话框的结构与原理

1. 模态对话框

在涉及GUI程序开发的过程中,常常有模态对话框以及非模态对话框的概念

模态对话框:在子界面活动期间,父窗口是无法进行消息响应。独占用户输入
非模态对话框:各窗口之间不影响

模态框和非模态框的主要区别:

1.模态对话框会阻塞线程其他窗口的输入消息,其他窗口无法响应包括用户输入;
2.模态对话框会中断执行流程,关闭模态窗口,后会继续执行;

在用户层的主要逻辑如下: 

复制代码

TestDlg dlg;

if (dlg.DoModal() == IDOK)
{
           //处理完毕后的操作
}
.......//后续处理

复制代码

在具体实现中,有如下几个步骤:
1. 让父窗口失效 EnableWindow(parentWindow, FALSE)
2. 建立模态对话框自己的消息循环(RunModalLoop)
3. 直至接收关闭消息,消息循环终止,并销毁窗口。

复制代码

INT_PTR CDialog::DoModal()
{
    //对话框资源加载
    ......

    //在创建模态窗口之前先让父窗口失效,不响应键盘、鼠标产生的消息
    HWND hWndParent = PreModal();
    AfxUnhookWindowCreate();
    BOOL bEnableParent = FALSE;

    if (hWndParent && hWndParent != ::GetDesktopWindow() && ::IsWindowEnabled(hWndParent))
    {
        ::EnableWindow(hWndParent, FALSE);
        bEnableParent = TRUE;
                .......
    }

    //创建模态窗口,并进行消息循环,若窗口不关闭,则循环不退出
    AfxHookWindowCreate(this);
    VERIFY(RunModalLoop(dwFlags) == m_nModalResult);
            
    //窗口关闭,销毁窗口
    DestroyWindow();
    PostModal();

    //释放资源,并让父窗口有效
        pMainWnd->EnableWindow(TRUE);

        //返回
    return m_nModalResult;
}

复制代码

2. 模态窗口中的消息循环

复制代码

int CWnd::RunModalLoop(DWORD dwFlags)
{
    //要检查窗口状态是否是模态窗口
    //若状态一直为模态,则一直进行消息循环
    for (;;)
    {
        ASSERT(ContinueModal());

        // phase1: check to see if we can do idle work
        while (bIdle &&
            !::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE))
        {
            ASSERT(ContinueModal());

            // show the dialog when the message queue goes idle
            if (bShowIdle)
            {
                ShowWindow(SW_SHOWNORMAL);
                UpdateWindow();
                bShowIdle = FALSE;
            }

            // call OnIdle while in bIdle state
            if (!(dwFlags & MLF_NOIDLEMSG) && hWndParent != NULL && lIdleCount == 0)
            {
                // send WM_ENTERIDLE to the parent
                ::SendMessage(hWndParent, WM_ENTERIDLE, MSGF_DIALOGBOX, (LPARAM)m_hWnd);
            }
            if ((dwFlags & MLF_NOKICKIDLE) ||
                !SendMessage(WM_KICKIDLE, MSGF_DIALOGBOX, lIdleCount++))
            {
                // stop idle processing next time
                bIdle = FALSE;
            }
        }

        //在有消息的情况下取消息处理
        do
        {
            ASSERT(ContinueModal());

            // pump message, but quit on WM_QUIT
            if (!AfxPumpMessage())
            {
                AfxPostQuitMessage(0);
                return -1;
            }

            // show the window when certain special messages rec'd
            if (bShowIdle &&
                (pMsg->message == 0x118 || pMsg->message == WM_SYSKEYDOWN))
            {
                ShowWindow(SW_SHOWNORMAL);
                UpdateWindow();
                bShowIdle = FALSE;
            }

            if (!ContinueModal())
                goto ExitModal;

            // reset "no idle" state after pumping "normal" message
            if (AfxIsIdleMessage(pMsg))
            {
                bIdle = TRUE;
                lIdleCount = 0;
            }

        } while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE));
    }

ExitModal:
    m_nFlags &= ~(WF_MODALLOOP|WF_CONTINUEMODAL);
    return m_nModalResult;
}

复制代码

GetMessage与PeekMessage的区别:
GetMessage:用于从消息队列读取消息。若队列中没有消息,GetMessage将导致线程阻塞。
PeekMessage:检测队列中是否有消息,并立即返回,不会导致阻塞。

3. APP中的消息循环

复制代码

//thrdcore.cpp   
// main running routine until thread exits   
int CWinThread::Run()  
{  
   // for tracking the idle time state   
   BOOL bIdle = TRUE;  
   LONG lIdleCount = 0;  
  
   //消息读取乃至分发 当为WM_QUIT时,退出循环 
   for (;;)  
   {  
      //检查是否为空闲时刻
      while (bIdle &&  
            !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))  
      {  
         // call OnIdle while in bIdle state   
         if (!OnIdle(lIdleCount++))  
            bIdle = FALSE; // assume "no idle" state   
      }  
  
     //有消息,读消息并分发 
     do  
     {  
        // pump message, but quit on WM_QUIT   
        if (!PumpMessage())  
           return ExitInstance();  
  
        // reset "no idle" state after pumping "normal" message   
        if (IsIdleMessage(&m_msgCur))  
        {  
           bIdle = TRUE;  
           lIdleCount = 0;  
        }  
  
     }   
     while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));  
   }  
}

复制代码

4. 模态对话框中局部消息循环和APP全局消息循环的关系

4.1 APP消息循环和模态对话框中局部消息循环的关系

根据上图可以看出,在APP的消息循环再派发ONOK消息后,调用ModalDlg的响应函数,pWnd->OnOk();在该消息中,
会 进入模态对话框的消息循环,除非将模态对话框关闭,否则APP的DispatchMessage函数一直出不来。

一旦创建了模态对话框,进行局部消息循环,那么APP的消息循环就被阻断。整个程序的消息循环有模态对话框中得消息循环取代。所以给父窗口发送的非窗口消息,一样可以响应。

由于局部消息循环只在对话框中的一个响应函数中,而全局的消息循环也被阻断,局部循环一直运行,如果用户不进行处理并关闭模态对话框,该循环会一直不退出。其他对话框也得不到处理。

4.2 局部消息循环存在的必要性

我之前一直有这样一个疑问,觉得模态对话框中的局部消息循环没有必要,可以通过如下方式达到模态对话框的效果:

复制代码

pParentWnd->EnableWindow(FALSE);

CDialog *pDlg;
pDlg = new CDialog();
pDlg->Create();
pDlg->Show();

pParentWnd->EnableWindow(TRUE);

复制代码

并且做了个实验,貌似OK。但是这边有个疏漏的是,模态对话框的作用有两个:
1. 使父窗口失效,无法响应用户的输入
2. 在当前窗口为处理完毕时,禁止进入后续操作。
上述例子只达到了要求1,没有达到要求二

所以模态对话框中有如下代码:

1

if (dlg.DoModal() == IDOK)

若对话框没有关闭,是无法进行后续操作的。
但是按照我先前的理解,如果代码是这样的:

复制代码

void CAppDoModelTestApp::OnTestModaltest()
{
    CWnd* pMainWnd = AfxGetMainWnd();
    pMainWnd->EnableWindow(FALSE);

    m_pTestDlg1 = new CModalDlg();
    m_pTestDlg1->Create(IDD_DIALOG1);
    m_pTestDlg1->ShowWindow(SW_SHOW);

    m_pTestDlg2 = new CModalDlg();
    m_pTestDlg2->Create(IDD_DIALOG1);
    m_pTestDlg2->ShowWindow(SW_SHOW);
}

复制代码

在对话框TestDlg1后产生后,TestDlg2一样会出现。但是我们模态对话框希望的效果是:在TestDlg1未关闭前,TestDlg2不创建。所以此处体现出了局部消息循环的优势,就是在当前窗口为处理完毕时,一直循环, 拒绝进入后续代码中。

/*******************MFC创建模态对话框和非模态对话框的方法*********************/

在MFC中对话框有两种形式,一个是模态对话框(model dialog box),一个是非模态对话框(modeless dialog box)。本文对此分别简述其创建方法。

一、模态对话框(model dialog box)

在程序运行的过程中,若出现了模态对话框,那么主窗口将无法发送消息,直到模态对话框退出才可以发送。
点击模态对话框中的OK按钮,模态对话框会被销毁。
创建一个模态对话框的代码如下所示:

1

2

3

//创建一个模态对话框

CTestDialog td;

td.DoModal();

其中CTestDialog为我自己所新建的和一个对话框资源相关联的对话框类。
可以创建一个布局模态对话框类变量,不用担心它会随着所在函数返回而被销毁。因为DoModal()函数的一个功能是,当前只能运行此模态对话框,且停止主窗口的运行,直到模态对话框退出,才允许主窗口运行。
DoModal()函数也有显示对话框的功能,所以也无需调用其他函数来显示对话框。

二、非模态对话框(modaless dialog box)

在程序运行的过程中,若出现了非模态对话框,主窗口还可以发送消息。
点击非模态对话框中的OK按钮,非模态对话框没有销毁,只是隐藏了。若想点击OK按钮时,非模态对话框也销毁,那么CTestDialog类必须重载其基类CDialog的虚函数OnOK(),在此函数里调用DestroyWindow()来销毁此对话框。

此处采用和上面一样的方式来创建一个非模态对话框,代码如下:

1

2

3

CTestDialog td;

td.Create(IDD_DIALOG1); //创建一个非模态对话框

td.ShowWindow(SW_SHOWNORMAL); //显示非模态对话框

那么,在运行时,你会发现此对话框无法显示。这是因为你声明的对话框变量td是局部变量,但这个函数返回时,td也被析构了,所以无法显示此对话框。

创建非模态对话框,必须声明一个指向CTestDialog类的指针变量,且需要显示的调用ShowWindow()才能将对话框显示出来。有两种创建方法:

(1)采用局部变量创建一个非模态对话框

1

2

3

4

//采用局部变量创建一个非模态对话框

CTestDialog *pTD = new CTestDialog();

pTD->Create(IDD_DIALOG1); //创建一个非模态对话框

pTD->ShowWindow(SW_SHOWNORMAL); //显示非模态对话框

因为指针在声明的时候是被放在堆栈中,只有整个应用程序关闭后才会被销毁,所以可以正常显示对话框。
这种方法虽然不影响程序的运行,可是指针pTD所指向的内存却导致不可用,这样的编程很不好。

(2)采用成员变量创建一个非模态对话框
首先在你所要编写的类的头文件中声明一个指针变量:

1

2

private:

CTestDialog *pTD;

然后再在相应的CPP文件,在你要创建对话框的位置添加如下代码:

1

2

3

4

//采用成员变量创建一个非模态对话框

pTD = new CTestDialog(); //给指针分配内存

pTD->Create(IDD_DIALOG1); //创建一个非模态对话框

pTD->ShowWindow(SW_SHOWNORMAL); //显示非模态对话框

最后在所在类的析构函数中收回pTD所指向的内存:

1

delete pTD;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值