1、非模态对话框和父窗口共享当前线程的消息循环
2、模态对话框新建一个新的消息循环,并由当前消息循环派发消息,而父窗口。模态对话框屏蔽了用户对它父窗口的操作,但是不是在消息循环里面屏蔽,所以给父窗口发送消息,父窗口还是可以接收得到。
3、调用模态对话框的窗口处理函数会被阻塞,但是新的消息循环仍然可以调用父窗口的消息处理函数,所以,发送给父窗口的新消息仍然可以被及时处理。
/*****************************MFC模态对话框的消息循环***************************/
MFC模态对话框的消息循环
单线程程序, 当主窗口响应函数中弹出模态对话框时,为什么主窗口响应函数可能照常工作?
当弹出模态对话框时,线程的消息循环无法返回,父窗口的事件本应没人处理,应该处于卡死状态,但实事上父窗口是可以正常响应能接收到的消息的,比如计时器传来的WM_TIMER 及系统托盘菜单传回来的WM_COMMAND。
之前的消息循环无法返回是正确的,但模态对话框并不意味着死循环,实事上,它在做另一个消息循环。
AfxInternalPumpMessage() 里面就是一个消息泵,包括消息的获取与分发:
只要有消息循环存在线程内的所有窗口就会活过来。不管弹出多少个模态对话框,线程内始终有一个消息循环为所以的窗口服务。
但这种形式带来的那一个问题就是,如何关闭所有模态对话框并退出程序?
如果简单地有EndDialog来关闭对话框,是无法让所有的消息循环返回的。有一种做法是使用PostQuitMessage,使当前的消息循环退出,消息循环收到WM_QUIT后,不当退出本次循环,还会给线程消息队列Post另一个QUIT 消息,这样消息循环就接二连三挨个退出了。
int CWnd::RunModalLoop(DWORD dwFlags)中的代码片段可以说明这点:
/*************************windows 消息队列,消息循环,模态对话框**************/
Windows的消息队列是基于线程的。
消息队列,消息循环:
线程是程序串行执行的最小单位。
一个典型的Win32项目(不是MFC项目,只有一个窗口的项目),其中的消息循环会使用如下代码实现:
//代码段1
MSG msg
BOOL bRet;
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
GetMessage()是取得消息,如果没有消息,线程阻塞在这里,不占用CPU。
DispatchMessage()是将消息分发,分发到所有在这个线程中创建的窗口的窗口处理函数中去。 如果不需要分发消息,就不需要调用DispatchMessage(),例如线程中没有窗口的情况。
线程消息循环:
实际上,任何线程只要调用了上述代码段1中的代码,就已经实现了消息循环的功能。更进一步,其实只要调用GetMessage()函数就可以了。
即:
//代码段2
BOOL bRet; MSG msg;
while( (bRet = GetMessage(&msg, NULL, 0, 0)) != 0)
{
if (bRet == -1 || WM_QUIT == msg.message){
// handle the error and possibly exit
break;
}
}
在工作线程中,只要有代码段2,就可以实现消息循环的功能了。
这是一个很方便的线程间异步通信的机制。给线程发消息用PostThreadMessage()。为什么去掉了DispatchMessage()呢,因为一般情况下,在工作线程中是不需要创建窗口的,不需要分发到窗口中去处理,能从线程的消息队列里取得消息就足够了。
如果设计线程的初衷为:当某条件发生时,让工作线程开始工作,一直将工作完成,之后挂起,等待新条件的发生,等待时不占用CPU资源。 那么代码段2所展示的机制就能很好的完成这样的功能。 而且可以使多个工作线程都用同样的机制实现,彼此间协同工作,加之某种资源共享机制,可以实现一个异步的消息处理链。
模态对话框:
消息循环可以有多个,可以在上一级消息循环的某个消息的处理过程中,局部创建一个消息循环,模态对话框就是采用这种机制创建出来的。
一个线程可以有多个消息循环,并行的消息循环显然没有意义,多个就是在消息循环中嵌套消息循环。
如代码段1中的DispatchMessage函数,将消息派发到窗口的消息处理函数中,WndProc1()。
现在假设在WndProc1()中创建一个消息循环,并且只取得这个窗口本身的消息 GetMessage(&msg, hWnd, 0, 0),不必Dispatch了,因为已经找到了目标窗口,然后用这个窗口真正的消息处理函数去处理。 这样模态对话框存在时,处理了所有的窗口消息,创建模态对话框的窗口就一直得不到处理消息的机会,模块对话框就始终处于最上层,直到其主动退出。
当然,对模态对话框的这种解释是简化了的,实际过程可能复杂的多,取得的窗口消息至少要包含所有的子窗口的消息,也需要Dispatch到相应的子窗口,等等。但是最基本的机制应该就是这样的。
/*********************模态对话框的消息机制****************************/
当一个窗口时模态对话框时,它的消息处理过程是有点特殊的。模式对话框都有自己的消息循环,它阻塞的是原始的消息循环,但是被对话框的消息循环接替。消息循环的本
质是调用窗口过程,进一步调用你的各种消息响应函数,所以无论有多少个消息循环存在,只要有一个消息循环有效,所有的消息响应函数都能被调用,这也是为什么主窗口还能
响应消息的缘故。多个消息循环的存在会产生某些副作用,比如消息重入,第一次消息响应时弹出一个模式对话框,模式对话框的消息循环2取代原始消息循环1,假设此时主窗口
消息队列里又有一个同样的消息到来,消息循环2也会调用同样的窗口过程(响应函数),此时就能导致消息重入(因为第一次进入这个处理函数还没有返回,本质上这个响应函数
被递归调用了),这也是能弹出多个模式对话框的原因。每个模式对话框都有自己的消息循环,只有最后一个弹出对话框的消息循环才是活动的消息循环,其它所有消息循环(包
括主窗口、之前弹出的模式对话框)全被阻塞。这里的“阻塞”并不是消息被阻塞,而只是DispatchMessage一直没有返回而已,但是其它的消息循环会接替这些工作。
/**********************MFC模态模式对话框,MessageBox消息循环原理,定时器TIMER消息响应处理函数重入************/
一、MessageBox和定时器TIMER
MessageBox是Win32 API全局函数,必须指定标题和样式。共有4个参数。没有父窗口就NULL。返回值是int,看选什么按钮。
比如:MessageBox( NULL, "选什么?", "标题", MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2);
::表示全局函数。
在MFC中,可使用全局API函数::MessageBox(NULL,………),子程序会被暂时中断在这个MessageBox上!其后的代码无法运行,等到MessageBox返回后会继续运行代码。但父窗口可以被点出来,其它程序段可以运行,比如其它按钮。 因为NULL没有指定父窗口,所以不会使父窗口无效!
MFC中直接使用MessageBox,则是使用CWnd类的MessageBox方法,这时父窗口无法被点出来,等同于::MessageBox(m_hWnd,………)。同时子程序被暂时中断在这个MessageBox上。但关了其中一个消息框,则父窗就可以点出来了。 原理应该是m_hWnd 指定了父窗,MessageBox使父窗口无效,然后进入自己的消息循环,关闭时,使父窗有效。
在TIMER的响应函数中,有MessageBox的话,则会暂时中断在这个MessageBox上,但当下个时间到时,又会出现一个新的MessageBox,且也是暂时中断在这个MessageBox上... 即出现了多个消息响应函数的例程!!多次重入!!每个例程都中断在MessageBox上,MessageBox返回后,例程会继续执行到结束! 各个消息框都是父窗的子窗,所以是平等的,所以各个消息框都可以点出来,但父窗点不出来。 可以关闭任何一个消息框,但是这个重入例程MessageBox之后的代码不会运行,要按倒序关闭其后的消息框后才会运行。
二、消息框原理分析:参考《深入探讨MFC消息循环和消息泵》和调试时断点看代码
注意:
MFC是单线程运行的。
MFC内部是有消息循环。
消息循环的DispatchMessage()会阻塞,需要等窗口消息处理函数处理完了以后,才会返回。
窗口消息处理函数是由操作系统调用的。
窗口消息处理函数是可以多次重入的!类似函数自己可以嵌套一样。
单线程运作:阻塞→重入→阻塞→重入。。。
时间到→操作系统放WM_TIMER到线程队列→MFC消息循环DispatchMessage()阻塞→系统调用窗口消息处理函数OnTimer()处理完→消息循环继续
操作系统一到时间就会放WM_TIMER到线程队列。因为是单线程,如果窗口消息处理函数如果一直没处理完,则WM_TIMER会堆积在队列中。
OnTimer()中有消息框时情况就复杂了。MessageBox内部是一个模态对话框,模态对话框有消息循环。
时间1到,系统放WM_TIMER到队列
↓
MFC程序主消息循环DispatchMessage()阻塞
↓
操作系统调用第1个OnTimer()
↓
MessageBox生成一个模态对话框对象1,显示出来,运行对话框内的消息循环1
↓
时间2到,系统放WM_TIMER到队列
↓
MessageBox1的消息循环1 DispatchMessage()阻塞
↓
操作系统调用第2个OnTimer() (函数重入了!)
↓
MessageBox生成一个模态对话框对象2,显示出来,运行对话框内的消息循环2
↓
时间3到,MessageBox2的消息循环2 DispatchMessage()阻塞,操作系统调用第3个OnTimer() (函数重入了!)。。。
即一个串一个,只有最后一个消息框的消息循环在起作用,前面的消息循环全部阻塞了。
OnTimer()是主窗(父窗)函数,所以各个MessageBox都是子窗,各个子窗是都可以点出来,但父窗会被子窗失效掉,所以父窗点不出来。
当任何一个子窗关掉后,会使父窗有效,这时父窗可以点出来了。
三、消息框对话框关闭时原理
关闭时是怎样?为什么关闭时是按倒序在执行代码?
MessageBox无法调试进入内部看代码,就用模态对话框来代替吧:
CAboutDlg dlgAbout;
dlgAbout.DoModal(); // DoModal就会生成模态对话框
CDialog::DoModal()里面有CreateRunDlgIndirect()。
CWnd::CreateRunDlgIndirect里面有RunModalLoop()。
CWnd::RunModalLoop里面有消息循环AfxPumpMessage(),这里一直进行消息循环!循环中还有CWnd::ContinueModal()判断m_nFlags标志看是否需要退出循环。
当关闭对话框时,如果点“确定”就是触发CAboutDlg::OnOK(),点“X”就是触发CAboutDlg::OnCancel()。 这两个函数进去都是运行EndDialog()。
CDialog::EndDialog里面先运行EndModalLoop(),然后是运行::EndDialog()。
CWnd::EndModalLoop()里面有
m_nFlags &= ~WF_CONTINUEMODAL; 把标志设置成不再继续运行对话框
PostMessage(WM_NULL); 发送一个空消息以确保消息队列中有消息,这样消息循环不会因为没有消息而阻塞
::EndDialog()无法进入看代码,功能是把对话框关了不显示,然后把父窗激活。
把对话框关了不显示,这样我们就点不到这个对话框,就不会再触发窗口消息处理函数了。
所以清楚了:
点击关闭其中一个消息框时(比如消息框1),生成了一个消息(这个消息是关联这个消息框1的),“最后一个”消息框(比如消息框3)的消息循环DispatchMessage()这个消息后,操作系统重入消息框1对象的OnOK()或OnCancel(),设置m_nFlags标志成不再继续运行,发送一个消息框1的消息WM_NULL,以及用::EndDialog()把消息框1销毁了并把父窗激活。
这个时候,这个消息框1对象仍存在,它的消息循环仍阻塞在DispatchMessage()中,只有等消息框3、消息框2的消息循环退出、OnTimer()退出后,才会从DispatchMessage()返回,然后发现m_nFlags不需要继续运行消息框了,于是退出消息循环,然后退出消息框对象,然后退出OnTimer(),消息循环就又交还给MFC程序的主循环了。
四、非模式对话框
非模对话框没有自己的消息循环,也不会使父窗失效。
用Create()来显示非模对话框,函数会立刻返回,所以必须要用new,这样对话框对象就保留在内存中。
如果用CAboutDlg dlgAbout;,则处理函数结束后,对象也被销毁,即对话框一显示就被关掉了。
CAboutDlg* pdlg = new CAboutDlg;
pdlg ->Create(IDD_ABOUTBOX); //非模式对话框,函数立刻返回,无自己的消息循环
pdlg->ShowWindow(SW_SHOW);
所以,关键是消息循环、消息处理函数重入。
/*****************模态对话框和非模态对话框的区别*********************/
模态对话框 操作模式上来讲 模态对话框在关闭对话框(OnOk,OnCancel,OnClose)这三个消息产生之前不可对此对话框以外的对话框进行操作 当上面3个消息产生后系统负责删除模态对话框资源 而非模态对话框可以进行其他操作 必须在三个消息发生后自己在析构函数里回收此对话框资源 比较麻烦 模态对话框用DoModal()可以负责产生,显示,销毁窗口 非模态对话框需要调用Create()然后在创建的时候WS_VISIBLE或者在创建都调用ShowWindow 进行显示 最后调用DestroyWindow() 然后自己删除掉对话框对象比较麻烦
按工作方式不同,可将对话框分成两类:
模式对话框(modal dialog box模态对话框):在关闭模式对话框之前,程序不能进行其他工作(如一般的“打开文件”对话框)
无模式对话框(modeless dialog box 非模态对话框):模式对话框打开后,程序仍然能够进行其他工作(如一般的“查找与替换”对话框)
两者的区别:
一. 非模态对话框的模板必须具有Visible风格(Visible=True),否则对话框将不可见,而模态对话框则无需设置该项风格。在实际编程中更加保险的办法是调用CWnd::ShowWindow(SW_SHOW)来显示对话框,而不管对话框是否具有Visible风格。
二. 非模态对话框对象是用new操作符来动态创建的,而不是以成员变量的形式嵌入到别的对象中或以局部变量的形式构建的。通常应在对话框的拥有者窗口类内声明一个指向对话框类的指针成员变量,通过该指针可访问对话框对象。
三. 通过调用CDialog::Create函数来启动对话框,而不是CDialog::DoModal,这是两者之间区别的关键所在。由于Create函数不会启动新的消息循环,对话框与应用程序共用同一个消息循环,这样对话框就不会垄断用户输入。Create在显示了对话框后就立即返回,而DoModal是在对话框被关闭后才返回的。由于在Create返回后,不能确定对话框是否已关闭,这样也就无法确定对话框对象的生存期,因此只好在堆栈中构建对话框对象,而不能以局部变量的形式来构建之。
四. 必须调用CWnd::DestroyWindow而不是CDialog::EndDialog来关闭非模态对话框。调用CWnd::DestroyWindow是直接删除窗口的一般方法。由于缺省的CDialog::OnOK和CDialog::OnCancel函数均调用EndDialog,故程序员必须编写自己的OnOK和OnCancel函数并且在函数中调用DestroyWindow来关闭对话框。
五. 因为是用new操作符构建非模态对话框对象,因此必须在对话框关闭后,用delete操作符删除对话框对象。在屏幕上一个窗口被删除后,框架会调用CWnd::PostNcDestroy,这是一个虚拟函数,程序可以在该函数中完成删除窗口对象的工作,具体代码如下
void CModelessDialog::PostNcDestroy
{delete this; //删除对象}
这样,在删除屏幕上的对话框后,对话框对象将被自动删除。拥有者就不必显式地调用delete来删除对话框对象了。
六. 必须有一个标志表明非模态对话框是否打开的。这样做的原因是用户有可能在打开一个模态对话框的情况下,又一次选择打开命令。程序根据标志来决定是打开一个新的对话框,还是仅仅把原来打开的对话框激活。通常可以用拥有者窗口中的指向对话框对象的指针作为这种标志,当对话框关闭时,给该指针赋NULL值,以表明对话框对象已不存在了。
例如:
创建模态对话框
CTestDlg dlg;
dlg.DoModal();
创建非模态对话框
CTestDlg * dlg = new CTestDlg;
dlg->Create(IDD_TEST_DLG);
dlg->ShowWindow(SW_SHOW);