MFC

MFC 프로그램의 흐름 살펴보기 - 인터널

디버그정 2008. 9. 1. 20:58

1. 예제
앞으로 MFC의 내부속으로 들어가 보기 위해 실제 예제 코드를 가지고 실험하게 될 것이다.
Visual C++ 6.0 sp5 기반으로 설명할 것이며 아래 코드가 실험용으로 사용하게 될 예제이다.
// Simple.hpp

#include <afxwin.h>

class CSimpleApp: public CWinApp

{

   public:

   virtual BOOL InitInstance();

};


MFC를 이용하려면 afxwin.h는 반드시 포함시켜야 한다. CWinApp를 비롯한 MFC에서 제공하는 주요 클래스와 함수들의 프로토타입들이 정의, 또는 선언되어 있는 헤더 파일이다.

CSimpleApp 클래스를 선언하면서 CWinApp를 상속받도록 했다.
CWinApp는 application object라 해서 어플리케이션의 framework을 제공하는 클래스이다.

CWinApp의 object는 하나의 프로그램에 꼭 하나만 필요하며 어플리케이션의 초기화, 실행, termination등 어플리케이션을 관리하는데 필요한 코드들이 encapsulation되어 있다. application object는 반드시 global object로 선언되어야 한다는 룰을 가지고 있는데 왜 전역 object로 선언되어야 하는지는 강좌를 진행하면서 차차 설명될 것이고 그외 CWinApp 클래스에 대한 자세한 사항은 MSDN을 참고하면 되겠다.

// Simple.cpp #include "Simple.cpp"

BOOL CSimpleApp::InitInstance()

{

   CFrameWnd *pFrame = new CFrameWnd;

   m_pMainWnd = pFrame;

   pFrame->Create(NULL, "Simple");

   pFrame->ShowWindow(SW_SHOW);

   pFrame->UpdateWindow(); return TRUE;

}

CSimpleApp app;

위의 코드를 보면 역시 main이 없이 CWinApp를 상속받아 CSimpleApp 클래스를 정의한 후 application object인 app라는 전역 object 하나를 선언했다.
코드에서 입력한 것은 그게 다 인데 빌드해서 실행해보면 윈도우가 생성이 될 것이다. 여기에 대체 어떤 구조가 숨어있길래 윈도우가 생성이 되는 것일까.


3 MFC 프로그램의 흐름

3.1 Windows 프로그램의 진입점(entry point)

보통 우리가 처음 프로그램을 배울 때 c프로그램으로 작성된 어플리케이션은 main부터 시작한다고 배웠지만 엄밀히 말해 커널이 어플리케이션을 메모리에 로딩하고 나서 호출하는 곳은 main이 아니다. 프로그램의 진정한 entry point C/C++ run-time startup 코드라고 불리는 함수가 따로 존재하며 이는 linker에 의해서 실행파일이 만들어질 때 합쳐진다. , main C/C++ run-time 코드에 의해 불려지는 함수의 하나라고 보면 되는 것이다.
그럼 유저 코드가 실행되는 최초의 시작 함수가 main인가 하면 C++ 환경으로 넘어오게 되면 그것도 틀린 말이 된다
.
이는 C/C++ run-time startup 코드가 필요한 이유, 즉 존재 이유와 관련이 있는데 startup 코드가 하는 일은 command-line argument와 환경변수(environment variables)를 채워넣고(main함수에 인자로 넘어오는 argc, argv, argp등의 변수를 의미한다.) malloc이나 calloc과 같은 메모리 할당 함수를 쓸 수 있도록 heap을 초기화 하며 글로벌로 선언된 C++ 클래스 객체를 초기화 하는 작업을 한다
.
, main이 실행되기 전에 global varible로 정의된 C++객체가 있다면 그 객체의 생성자(constructor)가 먼저 실행되는 것이다
.
이외에 C/C++ run-time startup 코드는 __error __argc, _osver와 같은 run-time global variable을 초기화하는 작업도 수행한다.1

윈도우 환경에서 startup 코드는 GUI 어플리케이션이라면 WinMainCRTStartup 함수가 불려지고 CUI용 어플리케이션이라면 mainCRTStartup2가 호출된다. 보통 startup 코드는 linker에 의해 선택되어 합쳐지는데 만약 CUI 기반 어플리케이션 프로젝트를 만들어 놓고 mainWinMain으로 바꿔 놓거나 GUI 기반 어플리케이션에서 WinMain main으로 코딩하면 link 타임때 startup함수를 찾지 못해서 unresolved 에러가 나게 되는 것이 이때문이다. MFC를 쓰는 어플리케이션은 GUI기반이므로 WinMainCRTStartup entry point이다.3

3.2 Application object의 생성자

예제 코드로 돌아가서 app를 전역으로 선언했으니 전술한 바와 같이 WinMainCRTStartup에 의해서 app의 생성자가 main보다는 먼저 실행될 것이다. 위의 예제 코드에서는 app의 생성자가 없지만, 일반적으로 어떤 클래스의 생성자가 실행되기 전에 상속받은 상위 클래스의 생성자가 먼저 실행되므로 CWinApp의 생성자가 실행된다.
CWinApp 클래스의 생성자를 보자.

CWinApp::CWinApp(LPCTSTR lpszAppName)

{

                  .................

 

        // initialize CWinThread state

        AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();

        AFX_MODULE_THREAD_STATE* pThreadState = pModuleState->m_thread;

        ASSERT(AfxGetThread() == NULL);

        pThreadState->m_pCurrentWinThread = this;          <-------- (1) 이 부분이 중요하다 

        ASSERT(AfxGetThread() == this);

                ....................

 

        // initialize CWinApp state

        ASSERT(afxCurrentWinApp == NULL); // only one CWinApp object please

        pModuleState->m_pCurrentWinApp = this;               <-------- (2) 이 부분도

        ASSERT(AfxGetApp() == this);

 

        // in non-running state until WinMain

        m_hInstance = NULL;

        m_pszHelpFilePath = NULL;

 

                .....................

 

}

AfxGetThread, AfxGetApp, AfxMessageBox와 같이 함수명에 Afx라는 접두사가 붙은 것은 어느 클래스에도 속해 있지 않은 MFC 전역 함수를 의미한다. 따라서 이들은 아무 곳에서나 호출될 수 있다.

먼저 pThreadState라는 로칼 포인터에 _AFX_CMDTARGET_GETSTATE 라는 매크로를 통해 쓰레드의 상태 정보를 가져오고 있다.

 

#define _AFX_CMDTARGET_GETSTATE() (m_pModuleState)

 

m_pModuleState AFX_MODULE_STATE 타입으로 정의된, CCmdTarget 클래스의 멤버 변수이다. CWinAppCCmdTarget으로부터 상속받았으므로 자신의 멤버 변수로 쓰는데는 문제가 없다. m_pModuleState는 현재 실행중인 모듈에 대한 상태 정보를 담고 있는 멤버 변수이다. 여기에는 실행중인 user application object, instance handle, resource handle, application name, OLE object, registered class등 모듈에 대한 여러 정보가 담겨있다.

(1) 부분을 보면 m_pCurrentWinThread 라는 멤버 변수에 this를 할당하고 있다.
여기서 this는 어떤 객체를 가리키는가. 바로
CWinApp를 상속받아 이 생성자까지 오게된 CSimpleApp 객체, app를 가리키는 포인터이다.

그 다음에 AfxGetThread()AfxGetApp() 함수를 보자.

CWinThread* AFXAPI AfxGetThread()

{

        // check for current thread in module thread state

        AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();

        CWinThread* pThread = pState->m_pCurrentWinThread;

 

        // if no CWinThread for the module, then use the global app

        if (pThread == NULL)

                pThread = AfxGetApp();   <---- 유저 객체를 리턴하는 함수이다.

 

        return pThread;

}

_AFXWIN_INLINE CWinApp* AFXAPI AfxGetApp()

{ return afxCurrentWinApp; }

afxCurrentWinApp는 다음과 같이 정의되어 있다.

#define afxCurrentWinApp    AfxGetModuleState()->m_pCurrentWinApp

application object를 포인트하는 멤버 변수 m_pCurrentWinApp를 반환하는 것인데 이는 위의 CWinApp 생성자에서 보다시피 유저 객체인 this, CWinApp객체를 할당했기 때문에 CWinApp객체에 대한 포인터가 반환된다.(더 정확히 말하면, CWinApp를 상속받은 CSimpleApp객체라고 해야한다. 그러나 편의상 이하부터는 CWinApp객체와 CSimpleApp객체, user application object는 모두 동일한 객체를 의미하는 것으로 하겠다.) 현재 실행중인 쓰레드가 주 쓰레드(primary thread)인 경우 AfxGetThread()에서 반환되는 포인터와 AfxGetApp()에서 반환되는 포인터는 실제로는 동일하다.

AfxGetThread()AfxGetApp()는 중요한 함수로 이렇게 이 함수들에 의해 구해진 포인터는 앞으로 InitApplication, InitInstance, Run, ExitInstance등을 호출할 때 사용된다.
CWinApp의 생성자가 하는 일은 결국 사용자가 글로벌로 선언한 application object를 현재 MFC가 인식할 수 있는 주 쓰레드 및 오브젝트로 할당시키는 역활을 하는 것이다.
달리 말하면

CSimpleApp app; 

라는 이 간단한 한 줄의 코드가 MFC와 유저의 객체가 서로 만나서 연결되는 '역사적인' 순간을 만드는 코드였던 것이다.


3.3 MFC 프로그램의 진입점(entry point)


CWinApp의 생성자를 빠져나오면 다시 run-time startup 코드인 WinMainCRTStartup으로 되돌아 온다.
파일을 죽 흝어보면 다음과 같이 뒷부분에 WinMain을 호출하는 부분이 보일 것이다.


void WinMainCRTStartup(void)
{
      int mainret;
     ................
 
#ifdef WPRFLAG
            mainret = wWinMain(
#else  /* WPRFLAG */
            mainret = WinMain(
#endif  /* WPRFLAG */
                       GetModuleHandle(NULL),
                       NULL,
                       lpszCommandLine,
                       StartupInfo.dwFlags & STARTF_USESHOWWINDOW
                        ? StartupInfo.wShowWindow : SW_SHOWDEFAULT
                      );
    
    .................   
}

참고로 MFC 내부 소스로 들어 가면 WinMain을 이름 그대로 쓰지 않고 전처리기(preprocessor)에서

_tWinMain으로 정의해서 쓰고 있다.4 (vc98\crt\src\tchar.h)


   // TCHAR.h
   #ifdef _UNICODE
       #define _tWinMain   wWinMain
   #else 
       #define _tWinMain   WinMain
   #endif 


extern "C" int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPTSTR lpCmdLine, int nCmdShow)
{
    // call shared/exported WinMain
    return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);  <--- 이 함수가 바로 MFC의 entry point이다.
}


_tWinmain함수는 윈도우 프로그램의 main으로서 mfcs42.lib에 포함되어 있다. 2k 미만의 크기를 가진 이 라이브러리는 MFC로 어플리케이션을 만들때 항상 link되어 유저가 만든 오브젝트와 함께 exe파일로 합쳐진다. 내용은 MFC의 main이라고할 수 있는 AfxWinMain을 호출하는 작업을 한다.


AfxWinMain은 MFC 주요 라이브러리인 mfc42.dll에 포함되어 있다. MFC의 main 함수라니까 상당히 복잡할 것 같은데 의외로 그리 복잡하지 않다. main코드의 전문은 아래와 같다.


int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPTSTR lpCmdLine, int nCmdShow)
{
      ASSERT(hPrevInstance == NULL);  // hPreInstance는 앞서 실행된 현재 프로그램, 즉 자신의 복사본 인스턴스 핸들.
                                     // 16비트 윈도우와의 호환성을 위해 존재하며 win32 어플리케이션에서는 
                                     // 기본으로 NULL이다.
 
      int nReturnCode = -1;
      CWinThread* pThread = AfxGetThread();  // CWinApp 생성자에서 연결된 user application object를 가져온다.
      CWinApp* pApp = AfxGetApp();           // 역시 동일.. :)
 
       // AFX internal initialization
       if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow)) // MFC의 초기화 작업을 하는 함수이다. 
                goto InitFailure;
 
       // App global initializations (rare)
       if (pApp != NULL && !pApp->InitApplication())  // 응용 프로그램의 초기화 작업 수행하는 가상함수
            goto InitFailure;
 
       // Perform specific initializations


       if (!pThread->InitInstance())   // 거의 모든 MFC 프로그래머들이 이 가상함수 내에서 어플리케이션의 창(메인)을 생성한다.  
       {
           if (pThread->m_pMainWnd != NULL)
           {
                TRACE0("Warning: Destroying non-NULL m_pMainWnd\n");
                pThread->m_pMainWnd->DestroyWindow();
           }
           nReturnCode = pThread->ExitInstance();
           goto InitFailure;
        }
        nReturnCode = pThread->Run();   // message loop를 실행한다.
 
InitFailure:
        AfxWinTerm();    // MFC 프로그램의 정리 작업 수행
        return nReturnCode;
}


AfxWinMain 함수에서 하는 일은, 먼저 user application object를 가져오고 MFC 클래스 내부 초기화 작업을 수행한 후 InitApplication5과 InitInstance 함수를 호출한다. InitApplication과 InitInstance는 유저가 application을 초기화 하고 메인 창을 생성하는 코드를 넣는 가상함수6이다.


이렇게 MFC 프로그램의 초기화와 메인 창이 생성이 되면 CWinApp 객체의 Run함수를 호출하여 메시지 루프를 수행하게 되는 것이다.

자, 여기서 CWinApp를 상속받는 user application object를 왜 전역으로 선언해야 하는지 그 이유가 드러난다. 만약 application object를 글로벌이 아닌 로칼로 선언한다면 어떻게 될까?


CWinApp 클래스의 생성자에서 user application object의 주소를 afxCurrentWinApp에 할당해야 하는데 CWinApp 객체를 함수의 로칼로 선언해버린다면 AfxWinMain이 실행되는 시점에서 객체가 보이지 않기 때문에 pThread 와 pApp 변수가 null을 받게 될 것이다. 이는 application object가 아예 없는 것과 같으므로 app 객체의 가상함수인 InitInstance를 호출하는 시점에서 access violation이 발생하게 되는 것이다.


3.4 프로그램 인스턴스 초기화


CWinThread::InitInstance는 프로그램 인스턴스의 초기화 작업을, CWinThread::ExitInstance는 프로그램 인스턴스를 정리하는 작업을 수행한다. 이 두개의 함수는 CWinThread 내에서 현재 쓰레드가 유효한지를 검서하는 것 이외에는 특별히 하는 일은 없다.

BOOL CWinThread::InitInstance()
{
        ASSERT_VALID(this);
 
        return FALSE;   // by default don't enter run loop
}
 
int CWinThread::ExitInstance()
{
        ASSERT_VALID(this);
        ASSERT(AfxGetApp() != this);
 
        int nResult = m_msgCur.wParam;  // returns the value from PostQuitMessage
        return nResult;
}


일반적으로 유저가 InitInstance를 재정의 해서 메인 창을 생성하는 코드를 넣는 것이 MFC 프로그램의 전형이다. 예제에서도 다음과 같이 CWinThread::InitInstance를 재정의 해서 메인 창을 생성하는 코드를 넣었다.

BOOL CSimpleApp::InitInstance()      <--- 가상함수이기 때문에 유저가 재정의할 수 있다.

     CFrameWnd *pFrame = new CFrameWnd; 
     m_pMainWnd = pFrame; 
 
     pFrame->Create(NULL, "Simple");  <--- 메인 창을 생성한다.
         ......
 
     return TRUE; 


메인 창은 메인 프레임 클래스인 CFrameWnd 객체를 선언해서 창을 생성하는데 이것은 또다른 과정을 밟아 나가야 한다. 그 과정은 MFC Main Window 생성흐름 페이지에서 설명한다.


AfxWinMain에서 이 함수를 호출하는 부분을 보면, InitInstance의 리턴값으로 TRUE 또는 FALSE가 가능한데 이들은 각각 초기화 성공과 실패를 의미한다. 만일 TRUE를 반환하면 CWinThread::Run이 호출되어 메시지 루프가 수행되지만, FALSE를 반환하면 메인 윈도우를 파괴하고 ExitInstance를 실행한 후 어플리케이션을 종료하게 된다. 어플리케이션 객체가 NULL이거나 유저가 재정의한 InitInstance에서 FALSE를 반환하게 되면 초기화 실패로 간주하게 된다.


3.5 메시지 루프

 

AfxWinMain코드를 보면, InitInstance에서 창이 생성되고 나면 드디어 메시지 루프를 수행하는 CWinThread::Run이 실행된다.7

int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPTSTR lpCmdLine, int nCmdShow)
{
           ......
   if (!pThread->InitInstance())    // 메인 윈도우 생성
   {
           ......
   }
           ......
 
    nReturnCode = pThread->Run();   // message loop를 실행한다.
           ......
}

기본적으로 CWinThread::Run은 WM_QUIT 메시지가 발생할 때까지 메시지 큐에서 반복적으로 메시지를 가져와 분배(dispatch)하는 작업을 수행하게 된다.
MSDN의 도움말을 보면 CWinThread::Run도 Overridables에 포함되어 있다. 즉, 이 함수도 가상함수라는 이야기인데 말이 가상함수지 실제로 메시지 루프가 바뀔 일은 거의 없으므로 유저가 재정의해서 쓸 일은 없다.
그럼 메시지 루프가 어떻게 이루어 지는지 살펴보자.


// main running routine until thread exits
int CWinThread::Run()
{
        ASSERT_VALID(this);
 
        // for tracking the idle time state
        BOOL bIdle = TRUE;
        LONG lIdleCount = 0;
 
        // acquire and dispatch messages until a WM_QUIT message is received.
        for (;;)    
        {
                // phase1: check to see if we can do idle work
                while (bIdle &&
                        !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
                {
                        // call OnIdle while in bIdle state
                        if (!OnIdle(lIdleCount++))
                                bIdle = FALSE; // assume "no idle" state
                }
 
                // phase2: pump messages while available
                do
                {
                        // pump message, but quit on WM_QUIT
                        if (!PumpMessage())
                                return ExitInstance();
 
                        // reset "no idle" state after pumping "normal" message
                        if (IsIdleMessage(&m_msgCur))
                        {
                                bIdle = TRUE;
                                lIdleCount = 0;
                        }
 
                } while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
        }
 
        ASSERT(FALSE);  // not reachable
}


이 함수의 흐름은 먼저 메시지 큐를 살펴봐서 메시지가 없을 때는 CWinThread::OnIdle을 호출하여 백그라운드 작업을 수행하고 메시지 큐에 메시지가 있다면 CWinThread::PumpMessage를 호출하여 메시지 큐로부터 메시지를 가져와 윈도우 프로시저에게 분배하는 작업을 수행하는 구조로 되어있다.


위에서 ::PeekMessage는 메시지 큐에 메시지가 있는지 채크하는 전역 함수이다. 메시지를 체크함과 동시에 메시지를 가져오는 작업도 함께 수행하는 ::GetMessage함수와는 달리 ::PeekMessage는 단지 메시지 유무만 검사하는 함수로 메시지가 있다면 TRUE를, 메시지가 없다면 FALSE를 반환한다.


메시지 큐에 메시지가 없을 때 실행되는 CWinThread::OnIdle도 가상함수이다. 즉, 사용자 입력이 없는 idle 상태일 때 어떤 작업을 하고자 할 때 이 함수를 재정의해서 쓰면 되는데 일례로 일정 시간이 지난 후에 강아지나 아이콘이 뛰어다니는 효과를 넣는 부분이 이 부분이다.


메시지 큐에서 메시지를 가져와서 분배하는 작업을 수행하는 CWinThread::PumpMessage 함수를 보자.


BOOL CWinThread::PumpMessage()
{
        ASSERT_VALID(this);
 
        if (!::GetMessage(&m_msgCur, NULL, NULL, NULL))   // 메시지 큐에서 메시지를 가져온다.
        {
                return FALSE;
        }
 
        // process this message
        if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
        {
                ::TranslateMessage(&m_msgCur);   // 키보드 이벤트 처리를 위한 함수
                ::DispatchMessage(&m_msgCur);   // 메시지 분배
        }
        return TRUE;
}

이 코드들은 win32 API로 윈도우즈 프로그램을 짜던 사람들에겐 매우 낯익은 로직일 것이다.
::GetMessage 함수로 메시지 큐에서 메시지를 가져와서 ::DispatchMessage를 통해 윈도우 프로시저에게 메시지를 분배하고 있다.

::GetMessage에서 WM_QUIT 메시지를 받으면 FALSE가 리턴되고 CWinThread::PumpMessage를 빠져 나가면서 역시 FALSE를 리턴한다. 그럼 CWinThread::Run에서 이를 받아 ExitInstance로 빠지게 되는 흐름을 가진다.


이는 WM_CLOSE 메시지 처리에서도 같은 흐름을 탄다. WM_CLOSE 메시지는 타이틀 바에 위치한 x표시로 되어 있는 닫기 버튼이나 콘텍스트 메뉴에서 닫기 메뉴를 눌렀을 때, 그리고 Alt + F4 를 눌렀을 때 발생하는데 WM_CLOSE 메시지가 발생한 후 그 다음에 WM_DESTROY 메시지가 발생하게 된다. 그리고 다음에 WM_NCDESTROY 메시지가 발생하는데 MFC의 윈도우 프로시저에서는 이 메시지를 만나면 PostQuitMessage(0)을 호출해서 WM_QUIT 메시지를 메시지 큐에 저장하게 된다.


3.6 메시지 전처리기

 

CWinThread::PumpMessage에서 메시지를 분배하기 전에 먼저 CWinThread::PreTranslateMessage라는 함수를 거쳐가게 되어 있는 것을 볼 수 있다.
CWinThread::PreTranslateMessage는 메시지 전처리기로서 클래스 계층도에 따라 CWnd::PreTranslateMessage, CFrameWnd::PreTranslateMessage등을 차례로 거쳐간다. 이 함수는 가상함수이기 때문에 프레임 윈도우 객체(CFrameWnd)에서 유저가 재정의할 수 있는 함수이다. 기본적으로 FALSE를 반환하게 되어 있으며 특정 메시지에 대해 TRUE를 반환시키면 윈도우 프로시저에 분배가 되지 않기 때문에 유저가 메시지를 가로채서 메시지 필터링 기능을 수행할 수 있다.

BOOL CWinThread::PreTranslateMessage(MSG* pMsg)
{
     ASSERT_VALID(this);
 
     // if this is a thread-message, short-circuit this function
    if (pMsg->hwnd == NULL && DispatchThreadMessageEx(pMsg))  // 쓰레드 메시지는 쓰레드 메시지 프로시저에서 처리
        return TRUE;
 
    // walk from target to main window
    CWnd* pMainWnd = AfxGetMainWnd();
    if (CWnd::WalkPreTranslateTree(pMainWnd->GetSafeHwnd(), pMsg)) // applicatoin object에 속해있는 각 윈도우 객체의
        return TRUE;                                             // 메시지 전처리기가 호출된다. 
                                                                  // CFrameWnd의 PreTranslateMeesage도 이 안에서 실행
    // in case of modeless dialogs, last chance route through main
    //   window's accelerator table
    if (pMainWnd != NULL)
    {
         CWnd* pWnd = CWnd::FromHandle(pMsg->hwnd);
         if (pWnd->GetTopLevelParent() != pMainWnd)
                return pMainWnd->PreTranslateMessage(pMsg);
     }
 
     return FALSE;   // no special processing
}



3.7 윈도우 프로시저


메시지 전처리기를 통과했으면 그 다음부터는 윈도우 메시지 처리 과정의 일반적 흐름을 밟아 나가게 된다. 가상 키 이벤트를 문자 메시지로 바꾸는 ::TranslateMessage와 윈도우 프로시저에 메시지를 분배하는 ::DispatchMessage함수가 차례대로 수행되는 것이다.
여기까지 왔다면 한가지 의문점이 들 것이다. MFC에서 실제로 메시지를 처리하는 윈도우 프로시저는 대체 어디에 있는가.


MFC에서 ::DIspatchMessage에 의해 메시지를 분배받는 함수는 AfxWndProcBase이다. 윈도우즈 운영체제로부터 메시지를 받기 위해선 이미 정의된 윈도우 프로시저 모양과 동일해야 한다. 따라서 AfxWndProceBase 함수를 보면 메시지가 발생한 윈도우 핸들과 메시지, wParam, lParam등을 인자로 받고 있다.


LRESULT CALLBACK AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
        AFX_MANAGE_STATE(_afxBaseModuleState.GetData());
        return AfxWndProc(hWnd, nMsg, wParam, lParam);
}

AfxWndProcBase에서 호출하는 AfxWndProc 함수는 다음과 같은 내용을 가지고 있다.

LRESULT CALLBACK
AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
        // special message which identifies the window as using AfxWndProc
        if (nMsg == WM_QUERYAFXWNDPROC)
                return 1;
 
        // all other messages route through message map
        CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);  // HWND로부터 CWnd 객체를 얻어온다.
        ASSERT(pWnd != NULL);
        ASSERT(pWnd->m_hWnd == hWnd);
        return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}


이 함수가 하는 일은 윈도우 핸들(HWND)로부터 CWnd 객체를 얻어와서 AfxCallWndProc을 호출하는 것이다.
AfxCallWndProc 내부로 들어가면 다음과 같은 코드로 되어 있다.

// Official way to send message to a CWnd
 
LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,
        WPARAM wParam = 0, LPARAM lParam = 0)
{
            ..........
    LRESULT lResult;
    TRY
    {
            ..........
 
        // special case for WM_INITDIALOG
        CRect rectOld;
        DWORD dwStyle = 0;
        if (nMsg == WM_INITDIALOG)
                _AfxPreInitDialog(pWnd, &rectOld, &dwStyle);
 
        // delegate to object's WindowProc
        lResult = pWnd->WindowProc(nMsg, wParam, lParam);  <-- 이 함수가 MFC의 윈도우 프로시저이다.
 
        // more special case for WM_INITDIALOG
        if (nMsg == WM_INITDIALOG)
                _AfxPostInitDialog(pWnd, rectOld, dwStyle);
    }
    CATCH_ALL(e)
    {
        lResult = AfxGetThread()->ProcessWndProcException(e, &pThreadState->m_lastSentMsg);
        TRACE1("Warning: Uncaught exception in WindowProc (returning %ld).\n",
                lResult);
        DELETE_EXCEPTION(e);
    }
    END_CATCH_ALL
 
    pThreadState->m_lastSentMsg = oldState;
    return lResult;
}


그렇게 어려운 코드는 아니다.
WM_INITDIALOG 메시지에 대해서는 전처리와 후처리를 여기서 해주는데 모달 대화상자(modal dialog box)를 윈도우 스타일에 따라 화면 중앙으로 좌표를 조정하는등의 작업을 한다. 여기서는 중요한 내용이 아니므로 자세한 설명은 하지 않는다.
MFC에서 윈도우 프로시저 기능은 CWnd::WindowProc이 담당한다.


LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
    // OnWndMsg does most of the work, except for DefWindowProc call
    LRESULT lResult = 0;
    if (!OnWndMsg(message, wParam, lParam, &lResult))
        lResult = DefWindowProc(message, wParam, lParam);
    return lResult;
}

CWnd::WindowProc은 CWnd::OnWndMsg를 호출하여 메시지 맵에 등록된 메시지 핸들러를 호출하고, 만일 처리되지 않은 메시지가 있다면 CWnd::DefWindowProc을 호출하여 디폴트 방식으로 메시지를 처리한다.
CWnd::DefWindowProc는 다음과 같이 구현되어 있다.


LRESULT CWnd::DefWindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
    if (m_pfnSuper != NULL)
        return ::CallWindowProc(m_pfnSuper, m_hWnd, nMsg, wParam, lParam);
 
    WNDPROC pfnWndProc;
    if ((pfnWndProc = *GetSuperWndProcAddr()) == NULL)
        return ::DefWindowProc(m_hWnd, nMsg, wParam, lParam); 
    else
        return ::CallWindowProc(pfnWndProc, m_hWnd, nMsg, wParam, lParam);
}

m_pfnSuper에 윈도우 프로시저 함수가 설정되어 있지 않으면 기본적으로 win32 API에서 제공되는 ::DefWindowProc가 호출된다.
m_pfnSuper는 윈도우 프로시저 함수를 가리키는 함수 포인트 타입(WNDPROC)으로 정의된 CWnd 클래스의 멤버 함수이다. 현재 실행중인 어플리케이션 윈도우가 서브클래싱(subclassing)되어 있다면 m_pfnSuper가 가리키는 윈도우 프로시저가 호출된다. m_pfnSuper는 protected 멤버이기 때문에 직접 할당할 수는 없고 CWnd::SubclassWindow로 윈도우를 서브클래싱하면 서브클래싱 객체의 윈도우 프로시저로 할당이 된다.


CWnd::DefWindowProc도 가상함수로 유저가 재정의할 수 있다. 그러나 DefWindowProc에서는 윈도우의 모든 메시지를 처리해야하기 때문에 유저가 재정의해서 쓸 일은 거의 없다.


메시지가 처리되는 자세한 흐름은 MFC 메시지 처리 흐름 페이지에서 다루도록 한다.


3.8 어플리케이션 종료어플리케이션이 종료되는 흐름은 위의 메시지 루프 과정에서 설명하였다.
좀더 자세히 설명하면, 유저가 타이틀 바에 있는 닫기 버튼을 누르거나 ALT + F4를 누르면 제일 먼저 WM_SYSCOMMAND 메시지가 발생한다. 메시지 핸들러에서 이에 대한 처리를 하지 않으면 DefWindowProc으로 넘어가고 그곳 내부에서 WM_CLOSE 메시지를 발생시켜서 메시지 큐에 삽입한다. WM_CLOSE 메시지에 대한 처리를 유저측에서 하지 않으면 역시 DefWindowProc에서 처리하고


WM_DESTROY 메시지를 발생시키는데 WM_DESTROY 메시지를 받으면 DefWindowProc에서는 바로 WM_NCDESTROY를 발생시킨다. MFC 내부에서는 이 메시지를 감시하는 부분이 있어 WM_NCDESTROY가 발생하면 CWnd::OnNcDestroy가 실행되고 이 함수에서는 PostQuitMessage(0)를 통해 WM_QUIT 메시지를 메시지 큐에 삽입하는 작업이 이루어진다.


::GetMessage는 WM_QUIT 메시지를 받으면 FALSE가 리턴되고 CWinThread::PumpMessage도 빠져나와 CWinThread::Run에서 CWinThread::ExitInstance로 빠져나가게 된다.
CWinThread::ExitInstance도 가상함수이기 때문에 유저가 종료 직전에 해야될 작업을 정의할 수 있다.


BOOL CWinThread::PumpMessage()
{
    if (!::GetMessage(&m_msgCur, NULL, NULL, NULL))  
    {
        return FALSE;         <---- WM_QUIT 메시지를 만나면 이 조건으로 들어온다.
    }
        ............
 
    return TRUE;
}


int CWinThread::Run() 

    for (;;)     
    { 
         .....
      
       do 
       { 
          // pump message, but quit on WM_QUIT 
          if (!PumpMessage()) 
              return ExitInstance();   <--- 종료때 해야할 작업을 가상함수로 구현하면 된다.
           .....
 
       } while (::PeekMessage( ...) );
    }
}



5 정리 : MFC 프로그램의 최소 요구 조건

  • CWinApp 의 파생 클래스가 존재해야 한다.
  • CWinApp 의 글로벌 객체가 선언 되어야 한다.
  • CWinApp 의 파생 클래스는 InitInstance 가상 함수를 재정의 해야 하며, InitInstance 에서 프레임 창 생성이나 기본 논리를 수행해야 한다. (InitInstance 함수에서 메세지 상자나 대화 상자를 나타낸 경우 반드시 FALSE 를 반환해야 한다.)