본문 바로가기
C & C++/MFC 컨트롤

[MFC] MFC 대화상자에서 초기화 및 종료 순서

by izen8 2011. 4. 13.

"메시지 처리에 대해서 잘 알고 있다면 다음 내용이 필요 없다."

MFC에서 기본적인 위도우 프레임웍이 설계되어 있고 그 곳에 맞게 필요한 코드를 넣어주면 된다.
프로그래밍에 있어서 가장 중요한게 자원 할당과 해제이다. 특히 C++에서는 메모리 관리를 프로그래머가 직접해주기 때문에 잘못된 메모리 관리는 시스템 전체에 안좋은 영향을 준다.

MFC에서  가장 무식하게 자원 할당과 해제를 하는 경우를 보면 생성자와 소멸자에 하는 경우이다. 실제 직접 생성자와 소멸자에 코딩하는 경우는 드물고 포인터 변수를 NULL로 초기화하는 정도이다.

그럼 MFC에서 실제 API 호출 순서를 보자. 응용프로그램 형태 중에서 대화상자를 중심으로 살펴보겠다.


Case 1) 대화상자 뛰우고 [X] 버튼  눌러 종료하기

Warning: no message line prompt for ID 0x8003.
[CD1Dlg] PreInitDialog()
[CD1Dlg] OnInitDialog()
[CD1Dlg] OnClose()
[CD1Dlg] OnCancel()
[CD1Dlg] OnDestroy()
[CD1Dlg] PostNcDestroy()
[CD1Dlg] OnNcDestroy()

Case 2) 대화상자 뛰우고 "Cancel"  버튼 눌러 종료하기

Warning: no message line prompt for ID 0x8003.
[CD1Dlg] PreInitDialog()
[CD1Dlg] OnInitDialog()
[CD1Dlg] OnCancel()
[CD1Dlg] OnDestroy()
[CD1Dlg] PostNcDestroy()
[CD1Dlg] OnNcDestroy()

 

Case 3) 대화상자 뛰우고 "Ok" 버튼 눌러 종료하기

Warning: no message line prompt for ID 0x8003.
[CD1Dlg] PreInitDialog()
[CD1Dlg] OnInitDialog()
[CD1Dlg] OnDestroy()
[CD1Dlg] PostNcDestroy()
[CD1Dlg] OnNcDestroy()

 

* 처음

위의 경우를 살펴보면, 처음 시작시 호출되는 함수가,

PreInitDialog()
OnInitDialog()

두 함수가 기본적으로 호출이 된다. OnInitDialog 함수는 많이 익숙한 이름이다. 대화상자에 가장 기본적인 초기화 코드를 넣는 함수이다.

(함수라는 용어보다 메소드, API, 멤버 함수등을 적합한 이름을 선택해야되는데 편의상 쉽게 이해하기 위해서 함수로 통일한다.)

** 중간

그리고 종료하는 종류에 따라서 중간에 호출되는 함수가 달라진다.

[X]와 "Cancel" 버튼을 눌렀을 경우에 공통적으로 OnCancel 함수가 호출된다. 단, [X] 버튼을 눌렀을 경우 추가로 OnClose 함수가 호출된다.
간혹 OnClose 함수가 대화상자 닫히는(Close) 경우로 착각해서 이 곳에 넣는 경우가 있다. 나도 그런 적이 있었다. 위의 함수 호출 순서에 나왔듯이 OnClose 함수는 경우에 따라서 호출되지 않은다.
"Ok" 버튼을 눌렀을 경우는 역기서는 별다른 함수 호출이 없다. 필요하다면 OnOk 함수 정도 정의하면 된다.

*** 끝

종료하는 경우는 살펴보자.

OnDestroy()
PostNcDestroy()
OnNcDestroy()

세 종류의 함수가 호출된다. OnDestroy 함수는 메시지 핸들러로 WM_DESTROY 메시지를 수신하면 호출된다.
PostNcDestroy 와 OnNcDestroy 는 함수 재정의에 의해서 사용된다.
종료시 가장 많이 사용하는 함수가 OnDestroy와 OnNcDestroy 함수이다.
본인이 선호하는 것은 OnDestroy 함수이다. OnNcDestroy()는 함수를 재정의 해줘야하며 메시지 종료 (WM_DESTROY)에 의해서 종료될 경우 해당 자원 해제를 확인하는데 직관적이지 않기 때문이다.



소멸자와 생성자 사용에 대해서

앞의 예제를 실제 코드로 간략히 살펴보면, Class CD1Dlg 정의는 되어 있다고 가정한다.


CD1Dlg dlg; // (1)
if(dlg.DoModal() > 0) {
  // Ok
} else {
  // Cancel
}

와 같이 동작한다. 위의 코드에서 대화상자를 객체화하는 부분(1)를 저장해두었다가 필요시 호출해서 사용할 수 있다. 즉 다음과 같다.

if(m_pDlg == NULL) {m_pD1Dlg = new CD1Dlg; }
if(m_pD1Dlg->DoModal() > 0) {
  // Ok
} else {
  // Cancel
}

위와 같이 바뀌면 앞에서 초기화하는 부분

PreInitDialog()
OnInitDialog()

그리고, 종료하는 부분

OnDestroy()
PostNcDestroy()
OnNcDestroy()

이 호출하지 않는다고 생각할 수 있다. 근데, 전혀 그렇지 않다.
대화상자 DoModal함수 호출이 초기화하는 부분을 불러오게 된다.

PreInitDialog()
OnInitDialog()

종료하는 부분도 마찬가지이다. 그러면 대화상자 객체를 저장하는 이유는 뭘까?
대화상자를 빈번히 호출하면서 초기화가 오래걸리는 객체를 대화상자에 갖고 있다면, 한번 초기화한 후에 그 결과 객체를 계속 보관하고 필요시 가져와서 사용하면 성능 향상에 도움이 될 것이다.
(주! 이 부분은 여러 방법으로 가능하기 때문이 반드시 대화상자에 객체를 저장할 필요는 없다.)

그때 생성자에서 해당 객체를 생성하고 소멸자에서 해당 객체를 제거하면된다. 그러면 대화상자 뛰우고 종료하는 과정에서는 해당 객체가 사라지지 않는다.

 


 

프로그램 종료방법

응용 프로그램을 종료한다는 의미는 해당 프로그램의 가장 기본이 되는 윈도우를 종료 시키는 것과 같다. 따라서 MFC로 작성한 응용 프로그램의 기본 골격이 되는 윈도우는 CFrameWnd(CWnd, CDialog) 계열의 클래스일 것이고 이 윈도우를 종료 시키면 응용 프로그램은 종료하게 된다.

기본 골격이 되는 윈도우를 종료하는 가장 일반적인 방법은 해당 윈도우에 WM_CLOSE 메시지를 전달하면 된다.
이 메시지가 Main Frame 윈도우에 전달되면 처리기가 OnClose 함수를 호출하게 되고

OnClose함수는 저장하지 않은 작업을 저장하도록 요구하며 프로그램을 종료 시킨다. 아래의 코드는 WM_CLOSE 메시지를 전달하는 예제이다.

AfxGetMainWnd()->PostMessage(WM_CLOSE); 또는
AfxGetApp()->m_pMainWnd->PostMessage(WM_CLOSE)

위 예제에서 SendMessage를 사용하지 않고 PostMessage 함수를 이용하여 메시지를 전달했음을 주의해야 한다. SendMessage 함수는 자신이 전달한 메시지가 처리될 때까지 기다리기 때문에 메시지 처리가 끝나고 나면 이미 윈도우가 파괴된 이후가 될 것이다. 따라서 SendMessage(WM_CLOSE); 코드를 사용한 이후에는 윈도우 관련 함수를 사용하면 안 된다. 이 점만 주의 한다면 SendMessage사용해도 상관없다. 물론 PostMessage를 사용하면 이런 점은 고려하지 않아도 된다. 

이 함수는 응용 프로그램의 프로세스(Main Thread)가 처리중인 메시지 루프를 종료 시켜 응용 프로그램을 종료 시킨다. 따라서 WM_CLOSE 메시지를 전달하는 것보다 좀더 직접적인 응용 프로그램 종료 방법이다. 그러나 이 방법을 사용하면 프로세스가 종료되기 때문에 응용 프로그램 종료 도중에 종료를 취소할 수 없다. PostQuitMessage함수는 종료 상태를 지정하는 한 개의 인자를 가지며 아래와 같이 사용하면 된다.

PostQuitMessage(0);

응용 프로그램은 보통 한 개의 Thread로 구성되며 이 Thread가 응용프로그램 진행에 관련된 여러 가지 작업을 수행한다. 그 중에서도 가장 기본이 되는 것이 각종 메시지들에 대한 처리이다. 따라서 프로그램의 Main Thread는 메시지 처리 루프를 형성하게 되는데 이 메시지 루프의 종료 조건은 WM_QUIT이라는 메시지가 전달될 때까지 이다. 그리고 Main Thread는 메시지 루프를 종료한 직후, 응용 프로그램의 종료 루틴을 진행하게 된다.
결과적으로 PostQuitMessage는 프로그램의 Main Thread에게 WM_QUIT메시지를 전달하는 역할을 하는 함수임을 알 수 있다. 따라서 아래와 같이 사용해도 똑 같은 결과를 얻을 수 있다.

AfxGetMainWnd()->PosMessage(WM_QUIT); 

MFC는 프로그래머의 편의를 위하여 Application Wizard를 제공한다. 이 기능을 이용하여 MDI 또는 SDI 윈도우를 만들었을 때 만들어지는 여러 가지 소스와 자원 중에 메뉴 자원을 보면 Exit라는 항목이 존재하는데 우리가 특별한 코드를 작성하지 않아도 메뉴에서 이 항목을 선택하면 응용 프로그램이 종료한다. 즉, MFC가 기본적으로 이 메시지를 처리하고 있음을 알 수 있다. Exit 메뉴 항목의 속성을 보면 Command ID가 ID_APP_EXIT로 되어 있다. 그리고 메뉴 선택 시 발생하는 메시지는 WM_COMMAND이므로 아래와 같이 구성하여 Exit 항목을 선택한 것과 동일한 결과를 얻을 수 있다.

AfxGetMainWnd()->PostMessage(WM_COMMAND,ID_APP_EXIT);

사실 위 코드를 C++의 Default Parameter 기법을 이용하여 마지막 인자가 생략되어 있다. 따라서 좀더 정확하게 코드를 고쳐 쓰면 아래와 같다.

AfxGetMainWnd()->PostMessage(WM_COMMAND,ID_APP_EXIT,0);

여기에서 주의 해야 할 점은 마지막 인자를 이용하기 위해 0외에 다른 값을 사용해서는 안 된다는 것이다. 이 메시지는 메뉴에서 발생하도록 되어있던 것을 변칙적으로 사용하는 것이기 때문에 0외에 다른 숫자를 집어 넣으면 오류가 발생하게 된다.

 

 


 

 

 

 

개요..

 

윈도우 프로그램이 종료되는 메시지의 순서를 알고 적절한 대처를 합니다. 이것을 응용하면 윈도우가 죽기직전에 살릴 수도 있습니다.

사용자가 종료(X)버튼 클릭 -> WM_SYSCOMMAND -> WM_CLOSE -> WM_DESTROY -> WM_QUIT

코드
MFC 의 경우 b1 -> b2-> b3-> b4 의 순서로 코드가 진행됩니다.
 
void CMainFrame::OnClose()
{
    BOOL b1 = ::IsWindow( m_hWnd );        // b1 = TRUE
 
    // 윈도우를 살릴  있는 마지막 기회!!! (중요)
    if( 윈도우가 살아야함 == TRUE )
        return;
 
    CFrameWnd::OnClose();
 
    BOOL b4 = ::IsWindow( m_hWnd );        // b2 = FALSE 윈도우가사라졌다.
}
 
void CMainFrame::OnDestroy()
{
    BOOL b2 = ::IsWindow( m_hWnd );        // b1 = TRUE
 
    CFrameWnd::OnDestroy();
 
    BOOL b3 = ::IsWindow( m_hWnd );        // b1 = TRUE
}

 

댓글