7.WTL 분석

.CAppModule

WTL은 Win32나 MFC에 비해 문서화가 거의 되어 있지 않아 학습에 어려움이 많다. 그러나 템플릿이다 보니 헤더 파일에 소스가 전부 공개되어 있어 사실 아무 것도 숨기지 않는다는 면에서 분석해 보기는 오히려 더 쉽다. 소스를 분석해 보면 구조를 확실히 알 수 있으며 고급 문법도 맛볼 수 있다. 문제가 생겨도 라이브러리 안으로 파고 들어가 디버깅을 할 수 있으니 시간이 좀 걸리더라도 문제를 확실히 해결할 수 있다.

게다가 최신 비스타 UI의 적용 방법까지 발 빠르게 구경할 수 있어 소스 분석은 뿌리칠 수 없는 유혹이다. 단, 이렇게 소스를 마음 먹은대로 주물러 보려면 그 소스에서 사용한는 문법과 기반 API 구조에 대해서 아주 상세하게 잘 알고 있어야 한다. 여기서는 처음 만든 WTL 예제인 WTLFirst 예제를 통해 WTL의 내부를 분석해 보자. 엔트리 포인트는 WTLFirst.cpp 파일에 있다. 주석이나 ASSERT는 빼고 정리해 보면 다음과 같다.

 

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow)

{

     HRESULT hRes = ::CoInitialize(NULL);

     AtlInitCommonControls(ICC_COOL_CLASSES | ICC_BAR_CLASSES);

 

     hRes = _Module.Init(NULL, hInstance);

     int nRet = Run(lpstrCmdLine, nCmdShow);

     _Module.Term();

 

     ::CoUninitialize();

     return nRet;

}

 

COM 라이브러리와 공통 컨트롤을 초기화한다. 그리고 전역 객체인 _Module의 Init를 호출하여 응용 프로그램을 초기화하고 Run 함수를 호출하여 프로그램을 실행하며 Term으로 종료 처리한다. _Module은 소스의 상단에 선언되어 있으며 stdafx.h에 extern 선언되어 있는 전역 객체이다.

 

CAppModule _Module;

 

MFC의 theApp에 해당하는 전역 객체이며 응용 프로그램의 여러 가지 설정 정보를 가진다. CAppModule 클래스는 atlapp.h 헤더 파일에 정의되어 있다. ATL의 COM 서버 모듈을 표현하는 CComModule로부터 상속받아 응용 프로그램 전체를 대표한다. 메시지 루프와 세팅 변경 등의 응용 프로그램 전역적인 정보를 관리하는 역할을 한다.

 

class CAppModule : public ATL::CComModule

{

public:

     DWORD m_dwMainThreadID;

     ATL::CSimpleMap<DWORD, CMessageLoop*>* m_pMsgLoopMap;

     ATL::CSimpleArray<HWND>* m_pSettingChangeNotify;

 

Init와 Term 함수는 응용 프로그램을 초기화하고 종료 처리한다. 현재 스레드의 ID를 조사해 놓고 메시지 루프 객체를 저장할 빈 맵 객체를 생성해 두는 정도의 간단한 작업만 한다.

 

HRESULT Init(ATL::_ATL_OBJMAP_ENTRY* pObjMap, HINSTANCE hInstance, const GUID* pLibID = NULL)

{

     HRESULT hRet = CComModule::Init(pObjMap, hInstance, pLibID);

     m_dwMainThreadID = ::GetCurrentThreadId();

     typedef ATL::CSimpleMap<DWORD, CMessageLoop*>   _mapClass;

     m_pMsgLoopMap = NULL;

     ATLTRY(m_pMsgLoopMap = new _mapClass);

     m_pSettingChangeNotify = NULL;

     return hRet;

}

 

void Term()

{

     TermSettingChangeNotify();

     delete m_pMsgLoopMap;

     CComModule::Term();

}

 

응용 프로그램을 실행하는 주체는 Run 함수이며 응용 프로그램 모듈의 _tWinMain 바로 위에 정의되어 있다. 이 함수에 대한 원형 선언이 따로 없고 사용하기 전에 정의만 했다는 점인데 이런 기법은 C에서나 쓰지 C++에서는 잘 쓰지 않는 것이다.

 

int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)

{

     CMessageLoop theLoop;

     _Module.AddMessageLoop(&theLoop);

 

     CMainFrame wndMain;

 

     if(wndMain.CreateEx() == NULL)

     {

          ATLTRACE(_T("Main window creation failed!\n"));

          return 0;

     }

 

     wndMain.ShowWindow(nCmdShow);

 

     int nRet = theLoop.Run();

 

     _Module.RemoveMessageLoop();

     return nRet;

}

 

메시지 루프 객체 하나를 생성하여 모듈에 추가한다. 그리고 메인 윈도우 객체를 생성하는데 이 과정에서 메인 윈도우의 차일드들이 줄줄이 생성된다. 툴바, 상태란이 붙고 뷰가 생성되며 뷰의 차일드들도 이때 생성될 것이다. 만약 이 과정에서 조금이라도 에러가 발생하면 응용 프로그램은 바로 종료된다. 무사히 초기화가 완료되었으면 메시지 루프 객체의 Run 함수를 호출하여 운영체제나 사용자로부터 전달되는 메시지를 처리한다.

 

int Run()

{

     BOOL bDoIdle = TRUE;

     int nIdleCount = 0;

     BOOL bRet;

 

     for(;;)

     {

          while(bDoIdle && !::PeekMessage(&m_msg, NULL, 0, 0, PM_NOREMOVE))

          {

              if(!OnIdle(nIdleCount++))

                   bDoIdle = FALSE;

          }

 

          bRet = ::GetMessage(&m_msg, NULL, 0, 0);

 

          if(bRet == -1)

          {

              ATLTRACE2(atlTraceUI, 0, _T("::GetMessage returned -1 (error)\n"));

              continue;   // error, don't process

          }

          else if(!bRet)

          {

              ATLTRACE2(atlTraceUI, 0, _T("CMessageLoop::Run - exiting\n"));

              break;   // WM_QUIT, exit message loop

          }

 

          if(!PreTranslateMessage(&m_msg))

          {

              ::TranslateMessage(&m_msg);

              ::DispatchMessage(&m_msg);

          }

 

          if(IsIdleMessage(&m_msg))

          {

              bDoIdle = TRUE;

              nIdleCount = 0;

          }

     }

 

     return (int)m_msg.wParam;

}

 

OnIdle에 대한 처리와 PreTranslateMessage 가상 함수를 호출하는 정도를 빼고 나면 그냥 단순한 메시지 루프일 뿐이다. MFC에도 비슷한 형식의 Run 메소드가 CWinThread에 정의되어 있다. 이 함수는 형태상으로 무한 루프로 되어 있으며 응용 프로그램이 종료될 때, 즉 WM_QUIT가 전달될 때까지 계속 실행된다. 이 함수가 리턴하면 응용 프로그램이 종료된다.

.메인 프레임

CMainFrame객체는 응용 프로그램의 메인 윈도우이다. 클래스 선언문을 보면 무려 4중 다중 상속을 받고 있는데 메인 윈도우의 기능 대부분은 CFrameWindowImpl 믹스인으로부터 상속받는다. 나머지 세 개의 기반 클래스는 상속이라기 보다는 단순히 기능을 위한 인터페이스만 제공하는 것이다.

 

class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI<CMainFrame>,

          public CMessageFilter, public CIdleHandler

{

public:

     //DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)

     static WTL::CFrameWndClassInfo& GetWndClassInfo()

     {

          static WTL::CFrameWndClassInfo wc =

          {

               { sizeof(WNDCLASSEX), 0, StartWindowProc,

                 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, NULL, NULL },

               NULL, NULL, IDC_ARROW, TRUE, 0, _T(""),MAINFRAME

          };

          return wc;

     }

 

선언문 선두에 DECLARE_FRAME_WND_CLASS라는 매크로가 있는데 전개해 보면 윈도우 클래스의 여러 가지 정보를 정의하여 리턴하는 GetWndClassInfo라는 정적 멤버 함수를 정의한다. 이 함수가 리턴하는 wc에는 클래스 이름, 배경색, 커서, 리소스 ID, 캡션 등의 정보들이 저장되어 있으며 이 정보는 이후 윈도우 클래스 등록이나 윈도우 생성에 사용될 것이다.

 

HWND CreateEx(HWND hWndParent = NULL, ATL::_U_RECT rect = NULL, DWORD dwStyle = 0, DWORD dwExStyle = 0, LPVOID lpCreateParam = NULL)

{

     const int cchName = 256;

     TCHAR szWindowName[cchName];

     szWindowName[0] = 0;

     ::LoadString(ModuleHelper::GetResourceInstance(), T::GetWndClassInfo().m_uCommonResourceID, szWindowName, cchName);

     HMENU hMenu = ::LoadMenu(ModuleHelper::GetResourceInstance(), MAKEINTRESOURCE(T::GetWndClassInfo().m_uCommonResourceID));

 

     T* pT = static_cast<T*>(this);

     HWND hWnd = pT->Create(hWndParent, rect, szWindowName, dwStyle, dwExStyle, hMenu, lpCreateParam);

 

     if(hWnd != NULL)

          m_hAccel = ::LoadAccelerators(ModuleHelper::GetResourceInstance(), MAKEINTRESOURCE(T::GetWndClassInfo().m_uCommonResourceID));

 

     return hWnd;

}

 

HWND Create(HWND hWndParent = NULL, ATL::_U_RECT rect = NULL, LPCTSTR szWindowName = NULL,

          DWORD dwStyle = 0, DWORD dwExStyle = 0,

          HMENU hMenu = NULL, LPVOID lpCreateParam = NULL)

{

     ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);

 

     dwStyle = T::GetWndStyle(dwStyle);

     dwExStyle = T::GetWndExStyle(dwExStyle);

 

     if(rect.m_lpRect == NULL)

          rect.m_lpRect = &TBase::rcDefault;

 

     return CFrameWindowImplBase< TBase, TWinTraits >::Create(hWndParent, rect.m_lpRect, szWindowName, dwStyle, dwExStyle, hMenu, atom, lpCreateParam);

}

 

CreateEx 함수는 문자열 리소스에서 윈도우 캡션, 메뉴 등을 읽고 윈도우를 생성한다. Create 함수에서 윈도우 클래스를 등록하고 스타일을 조사한 후 또 다른 Create 함수를 호출하는데 결국 이 안에서 CreateWindowEx API 함수로 윈도우를 생성한다. 생성에 성공하면 엑셀러레이터까지도 미리 로드해 놓는다. 윈도우가 생성되면 메시지 루프로 진입하여 프로그램이 실행될 것이다.

.메시지 맵

WTL의 메시지 맵은 MFC의 메시지 맵과 형태가 비슷하다. 그러나 내부 구조는 완전히 다르다. MFC의 메시지 맵은 메시지와 핸들러의 대응 관계를 정의하는 구조체 배열이며 구현 파일에 정의된다. 이에 비해 WTL의 메시지 맵은 메시지로부터 핸들러를 호출하는 코드이며 헤더 파일에 정의된다. 메인 프레임의 헤더 파일을 보면 다음 메시지 맵을 볼 수 있다.

 

     BEGIN_MSG_MAP(CMainFrame)

          MESSAGE_HANDLER(WM_CREATE, OnCreate)

          MESSAGE_HANDLER(WM_DESTROY, OnDestroy)

          COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)

          COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)

          COMMAND_ID_HANDLER(ID_VIEW_TOOLBAR, OnViewToolBar)

          COMMAND_ID_HANDLER(ID_VIEW_STATUS_BAR, OnViewStatusBar)

          COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)

          CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)

          CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)

     END_MSG_MAP()

 

마법사가 미리 만든 메시지 핸들러와 명령 핸들러, 체인 등이 포함되어 있는데 간략화하여 실제 코드로 전개해 보자. 메시지 맵에 두 개의 항목만 남기고 일단은 삭제한다.

 

     BEGIN_MSG_MAP(CMainFrame)

          MESSAGE_HANDLER(WM_CREATE, OnCreate)

          COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)

     END_MSG_MAP()

 

그리고 이 구문을 기계적으로 치환해 보면 다음 함수 하나가 정의된다. 실제로 이 함수로 치환한 채로 컴파일해도 잘 실행된다. WM_DESTROY를 빼서 정상 종료는 안되지만 말이다.

 

//   BEGIN_MSG_MAP(CMainFrame)

BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD dwMsgMapID = 0)

{

     BOOL bHandled = TRUE;

     switch(dwMsgMapID)

     {

     case 0:

     //MESSAGE_HANDLER(WM_CREATE, OnCreate)

          if(uMsg == WM_CREATE)

          {

              bHandled = TRUE;

              lResult = OnCreate(uMsg, wParam, lParam, bHandled);

              if(bHandled)

                   return TRUE;

          }

     //COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)

          if(uMsg == WM_COMMAND && ID_APP_EXIT == LOWORD(wParam))

          {

              bHandled = TRUE;

              lResult = OnFileExit(HIWORD(wParam), LOWORD(wParam), (HWND)lParam, bHandled);

              if(bHandled)

                   return TRUE;

          }

          break;

     //END_MSG_MAP()

     default:

          ATLTRACE(ATL::atlTraceWindowing, 0, _T("Invalid message map ID (%i)\n"), dwMsgMapID);

          ATLASSERT(FALSE);

          break;

     }

     return FALSE;

}

 

BEGIN_MSG_MAP 매크로는 함수의 선두를 정의한다. 모든 핸들러에서 공통적으로 사용되는 bHandled 변수를 선언 및 TRUE로 초기화하고 메시지 맵 ID에 따라 분기한다. 메시지 맵 ID는 0일 경우 현재 객체의 메시지를 의미한다. END_MSG_MAP() 매크로는 함수 선언문을 종료하고 틀린 메시지 맵 ID에 대해 에러 처리하는 역할을 한다. 이 두 매크로 사이에 메시지 맵이 작성된다.

MESSAGE_HANDLER 매크로는 if문으로 메시지 ID를 비교해 보고 메시지에 대응하는 함수를 호출한다. bHandled를 참조 인수로 넘겨 핸들러에서 처리했는지를 조사하고 그 결과를 다시 리턴할 뿐이다. COMMAND_ID_HANDLER 매크로는 WM_COMMAND 메시지에 대해 LOWORD(wParam)이 명령 ID와 일치하는지 보고 명령 ID에 대응하는 함수를 호출한다.

메시지 맵 함수는 WTL내의 다음 함수가 호출한다. 이 함수는 WTL내에서 생성되는 윈도우의 메시지 처리 함수이며 생성 과정에서 다소 복잡한 과정을 통해 이 함수가 윈도우 프로시저로 지정된다. Win32 프로젝트의 WndProc에 해당하는 함수라고 생각하면 된다.

 

template <class TBase, class TWinTraits>

LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

     CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;

     ....

     LRESULT lRes;

     BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);

 

핸들을 객체로 변환한 후 객체의 ProcessWindowMessage 함수를 호출하고 이 함수는 메시지 맵에 등록된 조건문을 순서대로 점검하여 핸들러를 호출한다. 처리 결과를 bRet로 받는데 이 값은 각 메시지 핸들러가 참조 호출로 리턴하는 bHandled값이다. bRet가 FALSE이면 디폴트 처리하되 WM_NCDESTROY같은 특수한 메시지를 제외하고는 대부분 DefWindowProc으로 보내진다.

 

이상으로 WTL의 내부를 약간, 아주 살짝쿵 들여다 보았는데 여기서 WTL 전체를 다 분석해 보고자 하는 것은 아니다. 마음 같아서는 안쪽 깊은 곳의 코드를 휘딱 까 뒤집어 보여 주고 싶은 마음 굴뚝같지만 시간이 허락하지 않음이 안타까울 뿐이다. 나도 내일까지 당장 해결해야 할 버그 목록이 잔뜩 쌓여 있는 개발자이다 보니 한가하게 글이나 쓰고 있을 여유가 없다. 이 엉망 진창인 강좌도 정말이지 없는 시간 쪼개 가며 쓴 것이니 부족한 부분이 있더라도 양해해 주기 바란다.

상세한 분석을 해 보지 못해 아쉽기는 한데 나는 다만 이런 식으로 안으로 조금만 추적해 들어가 보면 WTL 자체를 상세하게 탐구할 수 있다는 것을 알려 주고 싶을 뿐이다. 이후 누군가가 이 강좌를 기초로 하여 WTL 내부 구조를 분석한 강좌를 작성해 주면 참 고맙겠다. 이상으로 부족한 강좌를 마친다.