模態對話方塊可能導致程式崩潰

yousss發表於2018-08-24

在開發Windows引用程式的時候,在一些需要使用者確認,或者提示使用者注意的場合,經常使用模態對話方塊,或者叫模態視窗。在絕大多數情況下,模態視窗給開發人員帶來了極大的便利,並且在某些應用上有不可替代的優勢。然而凡事有利必有弊,如果不正確地使用模態視窗,卻有可能帶來某些嚴重問題,甚至可能引起程式崩潰。要想知道為什麼模態視窗可能帶來某些嚴重問題,就必須首先了解模態視窗的實現原理。因此本文將首先介紹模態視窗實現原理,然後分析為什麼會帶來問題。

原理

 

知道了原理,一切就可迎刃而解。瞭解了原理,就可以知道,模態視窗並不是Windows特有的,而是可以在任何一個GUI系統中實現出來,包括手機上。

因為Windows上的模態對話方塊為眾人所知,因此本文的例子都是指Windows上的,並且有時候會特指是MFC的。

眾所周知,當模態視窗被開啟之後,正常的流程會暫時掛起,或者通俗一點說,程式停住了,直到模態視窗關閉才會繼續執行。例如下面這段程式碼:

CInputDialog  dlg;

if(dlg.DoModal() == IDOK)

{

// 執行按了確定按鈕退出的流程

}

else

{

// 執行通過別的方式退出的流程,例如按了取消按鈕

}

// 繼續執行

在這段程式碼裡,在CInputDialog視窗關閉之前,註釋部分的程式碼是不會得到執行的。

接下來請先思考一個問題,為什麼呼叫了dlg.DoModal()之後,程式會停住呢?

首先,不可能是執行緒被掛起,因為一般情況,只有一個主執行緒,如果執行緒掛起,那就什麼也做不了了,但顯然模態視窗彈出來之後還是可以做很多事情的。

其次,也不可能是用類似於Sleep之類的函式,讓程式等待,和執行緒掛起一樣。

如果我們瞭解Windows應用程式的執行的原理,瞭解訊息分發的機制,就可以知道,UI執行緒有一個訊息迴圈,通過GetMessage之類的函式獲取訊息,並且分發。如果沒有這個訊息迴圈,整個視窗系統就無法正常工作。很顯然,當有模態視窗開啟的時候,整個視窗系統還是正常工作的,因此可以確定,此時訊息迴圈一定還在正常執行著。這個訊息迴圈在哪裡呢?因為當模態對視窗彈出來之後,程式就暫停了,相當呼叫模態視窗的函式一直沒有返回,那麼也就沒有機會再進入預設訊息迴圈了,這到底是怎麼回事呢?福爾摩斯經常說:“除去不可能的剩下的即使再不可能,那也是真相。”基於這個道理,真像只有一個,就是模態視窗內部有一個訊息迴圈,負責訊息的接收和轉發。

為了證明這個說法,可以做個試驗,彈出一個模態對話方塊,並設定合適的斷點,檢視堆疊。

使用DialogBox(NULL, MAKEINTRESOURCE(IDD_MAINDLG), m_hWnd, DialogProc);語句彈出對話方塊,並且在DialogProc裡設定一個合適的斷點,我們可以在堆疊中看到這樣的資訊:

         ZK.exe!CMainDlg::DialogProc(HWND__ * hwndDlg=0×000411e0, unsigned int uMsg=0×00000201, unsigned int wParam=0×00000001, long lParam=0×003a009c) 行90 C++

        user32.dll!_InternalCallWinProc@20() + 0×23 位元組     

        user32.dll!_UserCallDlgProcCheckWow@32() + 0xa9 位元組        

        user32.dll!_DefDlgProcWorker@20() + 0×7f 位元組       

        user32.dll!_DefDlgProcW@16() + 0×22 位元組        

        user32.dll!_InternalCallWinProc@20() + 0×23 位元組     

        user32.dll!_UserCallWinProcCheckWow@32() + 0xb3 位元組      

        user32.dll!_DispatchMessageWorker@8() + 0xe6 位元組     

        user32.dll!_DispatchMessageW@4() + 0xf 位元組 

        user32.dll!_IsDialogMessageW@8() - 0xeaa7 位元組 

        user32.dll!_DialogBox2@16() + 0xc0 位元組    

        user32.dll!_InternalDialogBox@24() + 0xb6 位元組        

        user32.dll!_DialogBoxIndirectParamAorW@24() + 0×36 位元組   

        user32.dll!_DialogBoxParamW@20() + 0×3f 位元組        

        ZK.exe!CMainDlg::OnOK(unsigned short __formal=0×0000, unsigned short wID=0×0001, unsigned short __formal=0×0000, unsigned short __formal=0×0000) 行98 + 0×1d 位元組 C++

上面的堆疊資訊中,紅色加粗的函式是API函式IsDialogMessage,這個函式的第二個引數是LPMSG lpMsg,這個正是從GetMessage返回的當前訊息的結構體。可以想象,在DialogBox函式內部的實現裡,在呼叫IsDialogMessage之前,必定先通過GetMessage之類的函式,從訊息隊裡返回了當前的訊息了。

到了這裡,我們基本可以確定,在模態視窗內部,也實現了一個訊息迴圈,真是這個訊息迴圈接管了執行緒中預設的訊息迴圈,使整個視窗系統能繼續正常的工作。同時由於訊息迴圈其實也是一個有退出條件的死迴圈,因此到這個迴圈結束之前(一般是關閉了模態視窗),模態視窗後面的程式碼是不會繼續執行的。

理解了模態視窗的原理,就可以在任何支援訊息佇列的GUI系統中,加入模態視窗的機制,這會減少很多開發工作。例如很多手機平臺不支援模態視窗,開發一些需要使用者確認的功能就比較麻煩,其實完全可以加入模態視窗,簡化開發。

注意事項

模態視窗極大地簡化了一些需要和使用者互動的操作,好處顯而易見。但這裡還是要指出一些需要注意的地方,否則使用的時候很可能會出問題。

影響PreTranslateMessage機制

在使用MFC,WTL等進行開發的時候,經常用到PreTranslateMessage機制,這個機制可以讓我們在訊息被派發之前先做一些事情。很多人以為PreTranslateMessage是Windows本身支援的,其實不然。PreTranslateMessage是MFC和WTL自己引入的一個概念,完全是和Windows無關的。在MFC和WTL的訊息迴圈中,這兩個庫的設計者在訊息分發之前,人為的加了一些程式碼,使得整個架構支援這一套機制。

正是如此,如果在正常的流程中彈出了模態視窗,就會使正常的PreTranslateMessage機制失效。因為模態視窗中已經包含了一個訊息迴圈,接管了執行緒中預設的訊息迴圈。而這個訊息迴圈是在DialogBox這個API函式中執行的,顯然不可能再有PreTranalateMessage機制了。

為了解決這一問題,只有讓模態視窗也使用和UI執行緒相同的訊息迴圈,MFC正是這麼做的。在MFC中,對話方塊類的DoModal函式,並不是呼叫DialogBox函式,而是直接使用CreateWindows建立一個非模態視窗,在視窗建立成功之後再呼叫MFC自己的訊息迴圈,這樣就可以讓PreTranslateMessage繼續生效。同時在視窗建立出來之後,必須再做一些別的操作,使這個模態視窗的父視窗失效(一般直接把視窗Disable掉)。同時訊息迴圈裡有合適的退出條件,並有恢復現場的一些操作,具體可以檢視MFC的DoModal函式。

WTL到目前為止,貌似暫時還沒有一個合適的方案來解決這個問題。事實上WTL的PreTranslateMessage機制實現的其實是有點問題的,或許以後會在這方面做一定的增強。

可能導致崩潰

這是一個嚴重問題,在條件合適的情況下,這個崩潰是必然的。

因為模態視窗彈出來之後,模態視窗後面的程式碼在視窗關閉之前將不會得到執行。然而此時整個視窗是在正常執行的,對於一些極端的情況,是極有可能造成崩潰的。下面看一個例子:

void CTestDlg::OnOK()

{

         CInputDialog dlg;

         If(dlg.DoModal() == IDOK)

         {

                   m_nValue = dlg.GetValue();

                   UpdateData(FALSE);

         }

}

這是一段典型的MFC程式碼,在絕大多數情況下,不會有任何問題。但是由於模態視窗彈出的時候,只是父視窗不能操作,但別的視窗完全還能正常執行,這時候就非常有可能由於某種原因,CTestDlg類已經銷燬了,而CInputDialog卻不知道,還在繼續執行,結果到了IDOK之後,對CTestDialog類的成員變數m_nValue賦值,就會出現崩潰了。

這個問題,如果在多執行緒的情況下,將會更加嚴重。因為在多執行緒的情況下,將會有更加多的不可預料的因素,所以使用的時候要更加小心。

相關文章