模态对话框和非模态对话框的消息循环分析
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);?
總結
以上是生活随笔為你收集整理的模态对话框和非模态对话框的消息循环分析的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 深入理解MFC消息循环和消息泵的原理
- 下一篇: 解决: VUE 项目中表单提交中文乱码、
