WTL을 사용하여 윈도우를 생성하는 것은, 기본적으로 ATL의 윈도우 생성 방식과 동일하다. WTL은 어느 날 갑자기 새롭게 생긴 혁신적인 물건이 아니라, ATL의 골격에 살을 덧붙인 것이다. WTL의 interface / implementation 분리 정책은 사실 ATL에서 온 것이며, ATL이 템플릿을 적극적으로 활용하는 것 또한 HP의 STL 구현 경험에서 영향을 받은 것이다. WTL을 이해하기 위해는, 먼저 ATL의 윈도우 생성, 메세지 전달 방식을 이해해야 한다.
ATL에서의 MFC CWnd와 같은 존재라면 CWindow인데, CWindow는 CWnd과 같지 않다. 인터페이스와 구현 모두를 가지고 있는 CWnd과 비교해서 CWindow는 그야말로 인터페이스만 가지고 있으며, 윈도우에 필요한 핸들러를 구현하는 것은 실제 구현을 담당하는 CWindowImpl을 상속 받아서 핸들러를 구현하면 된다.
class CMyWnd : public CWindowImpl< CMyWnd >
{
public:
DECLARE_WND_CLASS(_T("First WTL Window Class"))
};
이것만으로는 물론 실제 윈도우를 생성하는데 부족하다. 최상위의 윈도우를 만든다면 익숙한 프레임 윈도우 형태로 생성해야 한다. 즉, 윈도우 스타일을 지정해야 하는데, 그것은 어디에 있는가? 바로 여기서 ATL의 템플릿을 활용하는 장점이 나오는데, WinTraits을 사용하여 스타일을 지정하게 된다. WinTraits는 고전적인 윈도우 스타일, 확장 윈도우 스타일 모두 지정할 수 있다.
typedef CWinTraits< WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, WS_EX_APPWINDOW > CMyWndTraits
프레임 윈도우를 위한 것으로, CFrameWinTraits라는 것이 미리 정의되어 있는데, 이것을 사용하여 다시 한번 CMyWnd를 정의해보면 다음과 같다.
class CMyWnd : public CWindowImpl< CMyWnd, CWindow, CFrameWinTraits >
{
public:
DECLARE_WND_CLASS(_T("First WTL Window Class"))
};
이제 적당한 윈도우 스타일을 지정했으므로, 다음 작업을 진행해보자. MFC에서 CWnd를 상속받아 윈도우를 생성하기 위해서는 AfxRegisterWndClass()를 사용하여 윈도우 클래스를 지정해야 했다면, ATL에서는 보다 쉽게 매크로 하나로 처리할 수 있다. 이 부분이 MFC의 프레임워크보다 더 낫다고 생각되는 부분인데, MFC에서 AppWizard가 프로젝트를 생성하지 않았다면 메뉴와 같은 더미 리소스를 하나 생성해서 붙여줘야 했지만, ATL에서는 그냥 널 값을 넣어주어도 상관없다. 윈도우 클래스를 등록하는 것도 DECLARE_WND_CLASS처럼 보통 윈도우를 생성하는데 필요한 매크로와, 프레임 윈도우를 생성하는데 필요한 윈도우 클래스를 등록하는 DECLARE_FRAME_WND_CLASS 매크로가 준비되어 있다. 그리고, Microsoft API가 그렇듯이, EX 버전의 매크로도 역시 존재한다. DECLARE_WND_CLASS_EX는 배경색이나, 다른 몇몇 특정한 윈도우 클래스 스타일을 지정할 수 있도록 해준다. 다음은 메세지 핸들러를 등록하는 부분이다.
class CMyWnd : public CWindowImpl< CMyWnd, CWindow, CFrameWinTraits >
{
public:
DECLARE_WND_CLASS(_T("First WTL Window Class"))
BEGIN_MSG_MAP(CMyWnd)
END_MSG_MAP()
};
ATL 3.0 에서는 BEGIN_MSG_MAP 매크로 대신, BEGIN_MSG_MAP_EX 매크로를 사용해야 한다. ATL 7.0 / 7.1에서는 CWindowImpl / CDialogImpl 파생 클래스에 대해서는 BEGIN_MSG_MAP 매크로를 사용할 수 있지만, 그렇지 않은 클래스에 대해서는 BEGIN_MSG_MAP_EX 매크로를 사용해야 한다. atlcrack에 정의되어 있는 일반적인 윈도우 메세지를 처리하는 핸들러가 아니라면 MESSAGE_HANDLER 매크로를 사용하여 원하는 윈도우 메세지와 핸들러를 매핑시켜야 한다. 예를 들면, 직접 정의한 사용자 메세지의 경우 이 매크로를 사용해야 한다.
class CMyWnd : public CWindowImpl< CMyWnd, CWindow,CFrameWinTraits >
{
public:
DELCARE_WND_CLASS(_T("First WTL Window Class"))
BEGIN_MSG_MAP(CMyWnd)
ATL에서의 MFC CWnd와 같은 존재라면 CWindow인데, CWindow는 CWnd과 같지 않다. 인터페이스와 구현 모두를 가지고 있는 CWnd과 비교해서 CWindow는 그야말로 인터페이스만 가지고 있으며, 윈도우에 필요한 핸들러를 구현하는 것은 실제 구현을 담당하는 CWindowImpl을 상속 받아서 핸들러를 구현하면 된다.
class CMyWnd : public CWindowImpl< CMyWnd >
{
public:
DECLARE_WND_CLASS(_T("First WTL Window Class"))
};
이것만으로는 물론 실제 윈도우를 생성하는데 부족하다. 최상위의 윈도우를 만든다면 익숙한 프레임 윈도우 형태로 생성해야 한다. 즉, 윈도우 스타일을 지정해야 하는데, 그것은 어디에 있는가? 바로 여기서 ATL의 템플릿을 활용하는 장점이 나오는데, WinTraits을 사용하여 스타일을 지정하게 된다. WinTraits는 고전적인 윈도우 스타일, 확장 윈도우 스타일 모두 지정할 수 있다.
typedef CWinTraits< WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, WS_EX_APPWINDOW > CMyWndTraits
프레임 윈도우를 위한 것으로, CFrameWinTraits라는 것이 미리 정의되어 있는데, 이것을 사용하여 다시 한번 CMyWnd를 정의해보면 다음과 같다.
class CMyWnd : public CWindowImpl< CMyWnd, CWindow, CFrameWinTraits >
{
public:
DECLARE_WND_CLASS(_T("First WTL Window Class"))
};
이제 적당한 윈도우 스타일을 지정했으므로, 다음 작업을 진행해보자. MFC에서 CWnd를 상속받아 윈도우를 생성하기 위해서는 AfxRegisterWndClass()를 사용하여 윈도우 클래스를 지정해야 했다면, ATL에서는 보다 쉽게 매크로 하나로 처리할 수 있다. 이 부분이 MFC의 프레임워크보다 더 낫다고 생각되는 부분인데, MFC에서 AppWizard가 프로젝트를 생성하지 않았다면 메뉴와 같은 더미 리소스를 하나 생성해서 붙여줘야 했지만, ATL에서는 그냥 널 값을 넣어주어도 상관없다. 윈도우 클래스를 등록하는 것도 DECLARE_WND_CLASS처럼 보통 윈도우를 생성하는데 필요한 매크로와, 프레임 윈도우를 생성하는데 필요한 윈도우 클래스를 등록하는 DECLARE_FRAME_WND_CLASS 매크로가 준비되어 있다. 그리고, Microsoft API가 그렇듯이, EX 버전의 매크로도 역시 존재한다. DECLARE_WND_CLASS_EX는 배경색이나, 다른 몇몇 특정한 윈도우 클래스 스타일을 지정할 수 있도록 해준다. 다음은 메세지 핸들러를 등록하는 부분이다.
class CMyWnd : public CWindowImpl< CMyWnd, CWindow, CFrameWinTraits >
{
public:
DECLARE_WND_CLASS(_T("First WTL Window Class"))
BEGIN_MSG_MAP(CMyWnd)
END_MSG_MAP()
};
ATL 3.0 에서는 BEGIN_MSG_MAP 매크로 대신, BEGIN_MSG_MAP_EX 매크로를 사용해야 한다. ATL 7.0 / 7.1에서는 CWindowImpl / CDialogImpl 파생 클래스에 대해서는 BEGIN_MSG_MAP 매크로를 사용할 수 있지만, 그렇지 않은 클래스에 대해서는 BEGIN_MSG_MAP_EX 매크로를 사용해야 한다. atlcrack에 정의되어 있는 일반적인 윈도우 메세지를 처리하는 핸들러가 아니라면 MESSAGE_HANDLER 매크로를 사용하여 원하는 윈도우 메세지와 핸들러를 매핑시켜야 한다. 예를 들면, 직접 정의한 사용자 메세지의 경우 이 매크로를 사용해야 한다.
class CMyWnd : public CWindowImpl< CMyWnd, CWindow,CFrameWinTraits >
{
public:
DELCARE_WND_CLASS(_T("First WTL Window Class"))
BEGIN_MSG_MAP(CMyWnd)
MESSAGE_HANDLER(DEFINED_MSG, OnDefineMsg)
END_MSG_MAP()
LRESULT OnDefinedMsg(UINT msg, WPARAM wParam, LPARAM lParam)
{
....
}
};
여기서 또 하나, 재미있는 특징으로 CHAIN_MSG_MAP 매크로가 있다. 이것을 설명하기 위해서는 메세지 핸들러의 마지막 인자인 BOOL& bHandled를 설명해야 하는데, ATL은 윈도우 메세지를 해당 핸들러에게 넘기기 전에 bHandled를 TRUE로 설정한다. 메세지 핸들러가 리턴한 다음에 ATL의 디폴트 메세지 핸들러인 WindowProc에게 처리를 넘기고 싶다면, 이 값을 FALSE로 설정해야 한다. 명시적으로 base 클래스의 메세지 구현을 호출해야 하는 MFC와 다른 점이다. 처리되지 않은 메세지가 CHAIN_MSG_MAP 매크로를 만난다면, CHAIN_MSG_MAP에 명시된 클래스로 메세지가 전달된다. 메세지 전달 문제로 다중 상속이 지원되지 않는 MFC와 달리, ATL / WTL에서는 다중 상속을 통해서 해당 메세지를 처리할 클래스를 설정할 수 있으며, 이 메카니즘은 CHAIN_MSG_MAP을 통해 구현된다. 아주 극단적으로, 각각의 메세지를 처리할 클래스를 구성하고 각각의 클래스로 메세지를 넘길 수 있다. MFC가 처리되지 않은 메세지를 base 클래스로 처리를 넘기는 것과 구별되는 부분이다.
또, WM_COMMAND 메세지를 받아서 처리하기 위해서는, COMMAND_ID_HANDLER 매크로를 사용하여 해당 ID와 핸들러를 매핑하면 된다. 이 코드를 보면, PostQuitMessage(0)처럼, 과거 API만 사용하여 윈도우 프로그램을 만들던 옛 기억이 날 것이다. 그리고, 유쾌하지 않은, WPARAM과 LPARAM을 받아서 윈도우 메세지에 따라 직접 그 값을 풀어서 해석해야 하던 기억도 같이 떠올릴 것이다. ATL이 MFC에 비해 부족한 것이 바로 이 점인데, WTL은 이러한 ATL의 부족한 GUI 컴포넌트, 윈도우 처리 등을 보완하는 것이다. 그렇다면 WTL은 어떻게 이 메세지를 처리하는가? WTL의 atlcrack.h은 바로 이것을 위한 래핑 매크로를 제공한다.
class CMyWnd : public CWindowImpl< CMyWnd, CWindow, CFrameWinTraits >
{
public:
DECLARE_WND_CLASS(_T("First WTL Window Class"))
BEGIN_MSG_MAP(CMyWnd)
MSG_WM_CREATE(OnCreate)
MSG_WM_CLOSE(OnClose)
MSG_WM_DESTROY(OnDestroy)
END_MSG_MAP()
int OnCreate(LPCREATESTRUCT lpCreateStruct)
{
SetMsgHandled(FALSE);
return 0;
}
void OnClose()
{
SetMsgHandled(FALSE);
DestroyWindow();
}
void OnDestroy()
{
SetMsgHandled(FALSE);
PostQuitMessage(0);
}
};
위의 코드를 보면 MSG_로 시작하는 래핑 매크로를 볼 수 있는데, 이 매크로가 WPARAM, LPARAM의 값을 적절하게 풀어서 해당 윈도우 메세지에 맞게 함수를 재구성해주는 역할을 한다. 다만, 이 래퍼 매크로는 Visual Studio IDE 환경을 쓰더라도 IDE의 도움을 받을 수 있는 부분이 아니다. 즉, MFC에서 메세지 핸들러를 추가해주는 것처럼, 그런 수준의 도움은 없다는 뜻이다. 마이크로소프트(Microsoft)가 WTL을 공식적으로 지원하지 않기 때문인데, 불편하더라도 atlcrack.h를 열고 해당 함수의 원형을 찾아서 작성해주어야 한다. 래핑 매크로의 함수 리턴 타입과 시그내처들은 MFC의 메세지 핸들러와 대단히 비슷하기 때문에 MFC에 익숙한 사람이라면 쉽게 적응할 수 있을 것이다.
END_MSG_MAP()
LRESULT OnDefinedMsg(UINT msg, WPARAM wParam, LPARAM lParam)
{
....
}
};
여기서 또 하나, 재미있는 특징으로 CHAIN_MSG_MAP 매크로가 있다. 이것을 설명하기 위해서는 메세지 핸들러의 마지막 인자인 BOOL& bHandled를 설명해야 하는데, ATL은 윈도우 메세지를 해당 핸들러에게 넘기기 전에 bHandled를 TRUE로 설정한다. 메세지 핸들러가 리턴한 다음에 ATL의 디폴트 메세지 핸들러인 WindowProc에게 처리를 넘기고 싶다면, 이 값을 FALSE로 설정해야 한다. 명시적으로 base 클래스의 메세지 구현을 호출해야 하는 MFC와 다른 점이다. 처리되지 않은 메세지가 CHAIN_MSG_MAP 매크로를 만난다면, CHAIN_MSG_MAP에 명시된 클래스로 메세지가 전달된다. 메세지 전달 문제로 다중 상속이 지원되지 않는 MFC와 달리, ATL / WTL에서는 다중 상속을 통해서 해당 메세지를 처리할 클래스를 설정할 수 있으며, 이 메카니즘은 CHAIN_MSG_MAP을 통해 구현된다. 아주 극단적으로, 각각의 메세지를 처리할 클래스를 구성하고 각각의 클래스로 메세지를 넘길 수 있다. MFC가 처리되지 않은 메세지를 base 클래스로 처리를 넘기는 것과 구별되는 부분이다.
또, WM_COMMAND 메세지를 받아서 처리하기 위해서는, COMMAND_ID_HANDLER 매크로를 사용하여 해당 ID와 핸들러를 매핑하면 된다. 이 코드를 보면, PostQuitMessage(0)처럼, 과거 API만 사용하여 윈도우 프로그램을 만들던 옛 기억이 날 것이다. 그리고, 유쾌하지 않은, WPARAM과 LPARAM을 받아서 윈도우 메세지에 따라 직접 그 값을 풀어서 해석해야 하던 기억도 같이 떠올릴 것이다. ATL이 MFC에 비해 부족한 것이 바로 이 점인데, WTL은 이러한 ATL의 부족한 GUI 컴포넌트, 윈도우 처리 등을 보완하는 것이다. 그렇다면 WTL은 어떻게 이 메세지를 처리하는가? WTL의 atlcrack.h은 바로 이것을 위한 래핑 매크로를 제공한다.
class CMyWnd : public CWindowImpl< CMyWnd, CWindow, CFrameWinTraits >
{
public:
DECLARE_WND_CLASS(_T("First WTL Window Class"))
BEGIN_MSG_MAP(CMyWnd)
MSG_WM_CREATE(OnCreate)
MSG_WM_CLOSE(OnClose)
MSG_WM_DESTROY(OnDestroy)
END_MSG_MAP()
int OnCreate(LPCREATESTRUCT lpCreateStruct)
{
SetMsgHandled(FALSE);
return 0;
}
void OnClose()
{
SetMsgHandled(FALSE);
DestroyWindow();
}
void OnDestroy()
{
SetMsgHandled(FALSE);
PostQuitMessage(0);
}
};
위의 코드를 보면 MSG_로 시작하는 래핑 매크로를 볼 수 있는데, 이 매크로가 WPARAM, LPARAM의 값을 적절하게 풀어서 해당 윈도우 메세지에 맞게 함수를 재구성해주는 역할을 한다. 다만, 이 래퍼 매크로는 Visual Studio IDE 환경을 쓰더라도 IDE의 도움을 받을 수 있는 부분이 아니다. 즉, MFC에서 메세지 핸들러를 추가해주는 것처럼, 그런 수준의 도움은 없다는 뜻이다. 마이크로소프트(Microsoft)가 WTL을 공식적으로 지원하지 않기 때문인데, 불편하더라도 atlcrack.h를 열고 해당 함수의 원형을 찾아서 작성해주어야 한다. 래핑 매크로의 함수 리턴 타입과 시그내처들은 MFC의 메세지 핸들러와 대단히 비슷하기 때문에 MFC에 익숙한 사람이라면 쉽게 적응할 수 있을 것이다.