1. 程式人生 > >MFC 建立非模態對話方塊和銷燬過程

MFC 建立非模態對話方塊和銷燬過程

今天專案中遇到的問題,記錄下來,做個總結。

一個簡單的目的是建立一個非模態對話方塊並在對話方塊關閉後將其銷燬。

這裡的銷燬包括:銷燬對話方塊物件資源和對話方塊物件指標;

首先說建立對話方塊:

一、模態對話方塊(model dialog box)

在程式執行的過程中,若出現了模態對話方塊,那麼主視窗將無法傳送訊息,直到模態對話方塊退出才可以傳送。

點選模態對話方塊中的OK按鈕,模態對話方塊會被銷燬。建立一個模態對話方塊的程式碼:

  1. //建立一個模態對話方塊
  2. CTestDialog td;  
  3. td.DoModal();   
其中CTestDialog為所要建立的對話方塊類。而這個對話方塊是在另個類中建立。

因為DoModal()函式的一個功能是,當前只能執行此模態對話方塊,且停止主視窗的執行,直到模態對話方塊退出,才允許主視窗執行。

 DoModal()函式也有顯示對話方塊的功能,所以也無需呼叫其他函式來顯示對話方塊。(這裡就關係到DoModal中的函式呼叫順序,後面會有順序的說明

二、非模態對話方塊(modaless dialog box)

 建立非模態對話方塊,必須宣告一個指向CTestDialog類的指標變數,且需要顯示的呼叫ShowWindow()才能將對話方塊顯示出來。有兩種建立方法:

(1)採用區域性變數建立一個非模態對話方塊

  1. //採用區域性變數建立一個非模態對話方塊
  2. CTestDialog *pTD = new CTestDialog();  
  3. pTD->Create(IDD_DIALOG1); //建立一個非模態對話方塊
  4. pTD->ShowWindow(SW_SHOWNORMAL); //顯示非模態對話方塊 其中引數用swp_SHOWNOMAL,  SW_SHOW, SW_VISION 好像效果是一樣的 
這種方式建立的對話方塊,如果對話方塊物件指標是區域性的,那麼離開作用域,指標就消失掉,所以當釋放對話方塊物件指標所指的記憶體時就無法在建立對話方塊的類中進行釋放,若不做處理,則會造成記憶體洩露。開啟很多個對話方塊則會導致記憶體耗盡程式奔潰。

可以有兩種解決辦法:

1) 在建立對話方塊的類中,將對話方塊物件指標儲存起來,然後釋放對應指標,一般可以定義一個全域性的或者成員變數來儲存(用map)                         

2)將對話方塊物件指標的釋放交給對話方塊自身去維護,在上層類中之創建出來即可。具體方法是:在對話方塊類中重寫PostNcDestory方法,該方法中delete this即可。具體順序後面介紹。

(2)採用成員變數建立一個非模態對話方塊

        首先在你所要編寫的類的標頭檔案中宣告一個指標變數:

  1. private:  
  2.     CTestDialog *pTD;  

 然後再在相應的CPP檔案,在你要建立對話方塊的位置新增如下程式碼:

  1. //採用成員變數建立一個非模態對話方塊
  2. pTD = new CTestDialog(); //給指標分配記憶體
  3. pTD->Create(IDD_DIALOG1); //建立一個非模態對話方塊
  4. pTD->ShowWindow(SW_SHOWNORMAL); //顯示非模態對話方塊

        最後在所在類的解構函式中收回pTD所指向的記憶體:

  1. delete pTD;  

但這樣的方法只能建立一個對話方塊,若是再開啟一個對話方塊,則之前的指標丟失(會造成記憶體不好釋放)。

若要用這種方式建立多個對話方塊可以用一個map管理儲存指標,釋放時對應釋放,關閉了哪個對話方塊就把那個對應指標釋放掉,但這樣做很麻煩。

我的做法是第一種方法,在對話方塊類中維護釋放和關閉對話方塊。下面會將方法寫出來。

一個非模態的MFC 視窗的銷燬過程:

假設自己通過new建立了一個視窗物件pWnd,然後pWnd->Create。則銷燬視窗的呼叫次序:

1.手工呼叫pWnd->DestroyWindow(); // 一般在對話方塊類中的OnCancle函式中postMessage(WM_DESTORY)或者直接呼叫

2. DestroyWindow會發送WM_DESTROY;

3. WM_DESTROY對應的訊息處理函式是OnDestroy();

4. DestroyWindow會發送WM_NCDESTROY;

5. WM_NCDESTROY對應的訊息處理函式是OnNcDestroy;

6. OnNcDestroy最後會呼叫PostNcDestroy;

7. PostNcDestroy經常被使用者過載以提供釋放記憶體操作。例如可以使用delete this;

通過這種方式,視窗物件對應的視窗和視窗物件本身都被釋放了。

注:   銷燬視窗物件對應的視窗和釋放視窗物件指標 ,可以通過DestroyWindow,這是比較好的方法,因為最後MFC會自動相應WM_CLOSE導致CframWnd::DestroyWindow被呼叫,然後會一次釋放所有子視窗的控制代碼。使用者需要做的是在PostNcDestroy中釋放堆視窗物件指標。但因為某些物件是在棧中申請的,所以delete this可能出錯。這就要保證寫程式時自己建立的視窗儘量使用堆申請。

一個MFC視窗物件包括兩方面的內容:一是視窗物件封裝的視窗,即存放在m_hWnd成員中的HWND(視窗控制代碼),二是視窗物件本身是一個C++物件。要刪除一個MFC視窗物件,應該先刪除視窗物件封裝的視窗,然後刪除視窗物件本身。

刪除視窗最直接方法是呼叫CWnd::DestroyWindow::DestroyWindow,前者封裝了後者的功能。前者不僅會呼叫後者,而且會使成員m_hWnd儲存的HWND無效(NULL)。如果DestroyWindow刪除的是一個父視窗或擁有者視窗,則該函式會先自動刪除所有的子視窗或被擁有者,然後再刪除父視窗或擁有者。在一般情況下,在程式中不必直接呼叫DestroyWindow來刪除視窗,因為MFC會自動呼叫DestroyWindow來刪除視窗。例如,當用戶退出應用程式時,會產生WM_CLOSE訊息,該訊息會導致MFC自動呼叫CWnd::DestroyWindow來刪除主框架視窗,當用戶在對話方塊內按了OKCancel按鈕時,MFC會自動呼叫CWnd::DestroyWindow來刪除對話方塊及其控制元件。

對於一個在堆中動態建立的視窗物件,其生命期卻是任意長的。所以可能會產生這樣的疑問,為什麼有些程式用new建立了一個視窗物件,卻未顯式的用delete來刪除它呢?問題的答案就是有些MFC視窗物件具有自動清除的功能。

如前面講述非模態對話方塊時所提到的,當呼叫CWnd::DestroyWindow::DestroyWindow刪除一個視窗時,被刪除視窗的PostNcDestroy成員函式會被呼叫。預設的PostNcDestroy什麼也不幹,但有些MFC視窗類會覆蓋該函式並在新版本的PostNcDestroy中呼叫delete this來刪除物件,從而具有了自動清除的功能。此類視窗物件通常是用new操作符建立在堆中的,但程式設計師不必操心用delete操作符去刪除它們,因為一旦呼叫DestroyWindow刪除視窗,對應的視窗物件也會緊接著被刪除。

對於在堆中動態建立了的非自動清除的視窗物件,必須在視窗被刪除後,顯式地呼叫delete來刪除物件(一般在擁有者或父視窗的解構函式中進行).對於具有自動清除功能的視窗物件,只需呼叫CWnd::DestroyWindow即可刪除視窗和視窗物件。注意,對於在堆中建立的視窗物件,不要在視窗還未關閉的情況下就用delete操作符來刪除視窗物件。

下面總結一下MFC對話方塊建立和銷燬的函式呼叫順序:

非模態對話方塊

MFC應用程式建立視窗的過程

1.PreCreateWindow()   該函式是一個過載函式,在視窗被建立前,可以在該過載函式中改變建立引數   (可以設定視窗風格等等)

2.PreSubclassWindow()  這也是一個過載函式,允許首先子分類一個視窗

3.OnGetMinMaxInfo()   該函式為訊息響應函式,響應的是WM_GETMINMAXINFO訊息,允許設定視窗的最大或者最小尺寸

4.OnNcCreate()        該函式也是一個訊息響應函式,響應WM_NCCREATE訊息,傳送訊息以告訴視窗的客戶區 即將被建立

5.OnNcCalcSize()      該函式也是訊息響應函式,響應WM_NCCALCSIZE訊息,作用是允許改變視窗客戶區大小

6.OnCreate()          該函式也是一個訊息響應函式,響應WM_CREATE訊息,傳送訊息告訴一個視窗已經被建立

7.OnSize()            該函式也是一個訊息響應函式,響應WM_SIZE訊息,傳送該訊息以告訴該視窗大小已經 發生變化

8.OnMove()            訊息響應函式,響應WM_MOVE訊息,傳送此訊息說明視窗在移動

9.OnChildNotify()     該函式為過載函式,作為部分訊息對映被呼叫,告訴父視窗即將被告知一個視窗剛剛被建立

MFC應用程式關閉視窗的順序(非模態視窗)

1.OnClose()       訊息響應函式,響應視窗的WM_CLOSE訊息,當關閉按鈕被單擊的時候傳送此訊息

2.OnDestroy()     訊息響應函式,響應視窗的WM_DESTROY訊息,當一個視窗將被銷燬時,傳送此訊息

3.OnNcDestroy()   訊息響應函式,響應視窗的WM_NCDESTROY訊息,當一個視窗銷燬後傳送此訊息

4.PostNcDestroy() 過載函式,作為處理OnNcDestroy()函式的最後動作,被CWnd呼叫

對於非模態視窗,必須過載OnCancel函式,在函式中呼叫DestroyWindows()方法,且不能呼叫基類的函式。因為基類函式中呼叫的是 EndDialog()方法。(因為EndDialog是關閉模態對話方塊時呼叫的)而OnClose()也會呼叫OnCancel()方法。另外想通過OnOK關閉對話方塊,也必須同樣處理,不能直接用預設方法。

所以對於非模態視窗,其關閉過程為:

OnClose()->OnCancel()->DestroyWindow()->OnDestroy()->OnNcDestroy() ,->僅表示時間先後而已

而OnNcDestroy()最後又呼叫了PostNcDestroy()

說明:OnOK是對ID_OK的響應, OnCancel是對IDCANCEL的響應. 前者對應鍵盤的Enter, 後者對應Esc。

OnOK()和OnCancel()都呼叫了EndDialog().OnOK呼叫了UpdateData(TRUE)而OnCacel()沒有呼叫。
  在OnOK()結束剛進入DestroyWindow時,其實視窗並未關閉,依然可以用ShowWindow顯示出來

模態的對話方塊可以用EndDialog來銷燬, 非模態的對話方塊要用DestroyWindow來銷燬

MFC應用程式中建立模態對話方塊的函式呼叫順序:

1.DoModal()             過載函式,過載DoModal()成員函式

2.PreSubclassWindow()   過載函式,允許首先子分類一個視窗

3.OnCreate()            訊息響應函式,響應WM_CREATE訊息,傳送此訊息以告訴一個視窗已經被建立

4.OnSize()              訊息響應函式,響應WM_SIZE訊息,傳送此訊息以告訴視窗大小發生變化

5.OnMove()              訊息響應函式,響應WM_MOVE訊息,傳送此訊息,以告訴視窗正在移動

6.OnSetFont()           訊息響應函式,響應WM_SETFONT訊息,傳送此訊息,以允許改變對話方塊中控制元件的字型

7.OnInitDialog()        訊息響應函式,響應WM_INITDIALOG訊息,傳送此訊息以允許初始化對話方塊中的控制元件, 或者是建立新控制元件

8.OnShowWindow()        訊息響應函式,響應WM_SHOWWINDOW訊息,該函式被ShowWindow()函式呼叫

9.OnCtlColor()          訊息響應函式,響應WM_CTLCOLOR訊息,被父視窗傳送已改變對話方塊或對話方塊上面控制元件的顏色

10. OnChildNotify()     過載函式,作為WM_CTLCOLOR訊息的結果傳送

MFC應用程式中關閉模式對話方塊的順序

1.OnClose()        訊息響應函式,響應WM_CLOSE訊息,當"關閉"按鈕被單擊的時候,該函式被呼叫

2.OnKillFocus()    訊息響應函式,響應WM_KILLFOCUS訊息,當一個視窗即將失去鍵盤輸入焦點以前被髮送

3.OnDestroy()      訊息響應函式,響應WM_DESTROY訊息,當一個視窗即將被銷燬時,被髮送

4.OnNcDestroy()    訊息響應函式,響應WM_NCDESTROY訊息,當一個視窗銷燬以後被髮送

5.PostNcDestroy()  過載函式,作為處理OnNcDestroy()函式的最後動作被CWnd呼叫

對於DoModal出來的視窗,可以使用預設的OnOk()和OnCancel()來處理。其基類方法中會呼叫EndDialog()方法。

最後注意一個問題,通常我們建立一個非模態視窗時,可能會這樣寫

{

   CDialog * pWnd = new CMyDialog();

   pWnd->Create(……);

   pWnd->ShowWindow(SW_SHOW);

}

   一般是在一個模組或者一個函式中建立視窗,但是卻無法知道什麼時候關閉視窗。而pWnd也只是作為一個區域性變數。那麼如何對它進行析構呢?

通常這樣是過載虛擬函式PostNcDestroy()來實現

void CMyDialog::PostNcDestroy()
{
       CDialog::PostNcDestroy();
       delete this;
}

為什麼把對話方塊類的delete this放在PostNcDestroy中而不是OnNcDestroy

   這是因為OnNcDestroy只被已建立的視窗呼叫。如果建立視窗失敗(如PreCreateWindow), 則沒有視窗處來發送