MFC 메시지 맵의 구현과 동작 방식 {{{+1 메시지 맵 Message Maps (MFC) }}} 윈도우 프로그램을 만들 때 메시지가 오고가는 것을 제어하고 싶을 때, Win32 API 에서는 GetMessage()를 이용해 TranslateMessage()와 DispatchMessage() 를 이용해 윈도우 메시지를 제어했다. MFC에서 Win32 처럼 메시지를 받고 메시지를 처리하는 것이 메시지 맵이다. {{{+1 MFC의 메시지 전달 }}} MFC의 WinMain 함수가 시작되면 CWinApp와 CWinThread의 인스턴스를 하나씩 만든다. 그리고, CWinApp의 Run 메서드가 메시지 루프를 돌면서 CWnd* m_pMainWnd 멤버에 메시지를 보낸다. m_pMainWnd 은 CMainFrame : public CFrameWnd 이다. 그렇게 되면 메시지는 Chain of Responsibility 패턴으로 MainFrame -> MDI Child Frame -> (Pre) View -> Document -> Document Template 순서로 메시지가 전달된다. {{{+1 CCmdTarget }}} 대부분의 메시지는 프로그램에 대한 사용자의 반응이다. 커맨드는 메뉴 아이콘 또는 툴바 버튼 마우스 클릭이나 키 입력에 의해 생성된다. 사용자는 또한 윈도우 이동이나 리사이징 등으로 윈도우 메시지를 생성할 수도 있다. CCmdTarget 클래스는 커맨드 메시지들을 수신하고 응답할 수 있도록 도와준다. 그렇기 때문에 이런 기능을 필요로 하는 클래스들은 이 클래스를 상속하면 된다. CCmdTarget 클래스는 메시지 아이디를 그에 대응하는 커맨드 핸들러에 매핑해주는 메시지맵 인터페이스를 갖고 있다. 이 메시지맵 인터페이스를 통해서 커맨드 메시지는 이 메시지의 핸들러를 제공하는 클래스들을 순회하게 된다. {{{+1 struct AFX_MSGMAP }}} {{{#!gcode // afx.h ///////////////////////////////////////////////////////////////////////////// // Window message map handling struct AFX_MSGMAP_ENTRY; // declared below after CWnd struct AFX_MSGMAP { const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)(); const AFX_MSGMAP_ENTRY* lpEntries; }; }}} 실질적인 메시지 맵을 표현하는 구조체로 부모 클래스의 메시지 맵을 구하는 함수 포인터와 현재 클래스의 메시지 맵 엔트리 리스트 가지고 있다. MFC는 각 클래스 별로 메시지 맵을 연결 리스트로 유지한다. 현재 클래스의 메시지 맵에 명령에 해당하는 메시지 맵 엔트리가 없을 경우 부모 클래스 메시지 맵에서 해당하는 메시지 맵 엔트리가 있는지 검사한다. {{{+1 struct AFX_MSGMAP_ENTRY }}} {{{#!gcode struct AFX_MSGMAP_ENTRY { UINT nMessage; // windows message UINT nCode; // control code or WM_NOTIFY code UINT nID; // control ID (or 0 for windows messages) UINT nLastID; // used for entries specifying a range of control id's UINT_PTR nSig; // signature type (action) or pointer to message # AFX_PMSG pfn; // routine to call (or special value) }; }}} 메시지 맵에서 윈도우 메시지와 메시지를 처리하는 함수를 실질적으로 맵핑하는 구조체이다. {{{+1 DECLARE_MESSAGE_MAP() }}} 헤더에서 메시지 맵을 선언한다. 클래스 선언의 마지막에 사용하거나 DECLARE_MESSAGE_MAP()을 사용한 이후에 멤버를 선언할 경우 반드시 액세스 타입(private, public)을 새로 지정해줘야 한다. {{{#!gcode #define DECLARE_MESSAGE_MAP() \ protected: \ static const AFX_MSGMAP* PASCAL GetThisMessageMap(); \ virtual const AFX_MSGMAP* GetMessageMap() const; \ }}} {{{+1 BEGIN_MESSAGE_MAP(), END_MESSAGE_MAP() }}} 메시지 맵 구현 코드를 생성한다. 해당 클래스의 소스(.cpp)에 아래와 같은 형식으로 작성된다. {{{#!gcode BEGIN_MESSAGE_MAP(CMyView, CView) ON_WM_PAINT() END_MESSAGE_MAP() }}} 매크로에 의해 현재 클래스의 메시지 맵 참조로 확장된다. {{{#!gcode #define BEGIN_MESSAGE_MAP(theClass, baseClass) \ PTM_WARNING_DISABLE \ const AFX_MSGMAP* theClass::GetMessageMap() const \ { return GetThisMessageMap(); } \ const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \ { \ typedef theClass ThisClass; \ typedef baseClass TheBaseClass; \ static const AFX_MSGMAP_ENTRY _messageEntries[] = \ { #define END_MESSAGE_MAP() \ {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \ }; \ static const AFX_MSGMAP messageMap = \ { &TheBaseClass::GetThisMessageMap, &_messageEntries[0] }; \ return &messageMap; \ } \ PTM_WARNING_RESTORE }}} 즉, 다음 같이 변경된다. {{{#!gcode const AFX_MSGMAP* CMyView::GetMessageMap() const { return &CMyView::messageMap; } AFX_DATADEF const AFX_MSGMAP CMyView::messageMap = { &CView::messageMap, &CMyView::_messageEntries[0] }; const AFX_MSGMAP_ENTRY CMyView::_messageEntries[] = { ON_WM_PAINT() {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0} } }}} ON_WM_PAINT() 같은 message map entry macro들은 afxmsg_h. 파일에 선언되어 있다. 예를 들어 ON_WM_PAINT() 매크로는 다음과 같이 선언되어 있다. {{{#!gcode #define ON_WM_PAINT() \ { WM_PAINT, 0, 0, 0, AfxSig_vv, \ (AFX_PMSG)(AFX_PMSGW) \ (static_cast< void (AFX_MSG_CALL CWnd::*)(void) > ( &ThisClass :: OnPaint)) }, }}} {{{+1 DECLARE_DYNAMIC, IMPLEMENT_DYNAMIC, RUNTIME_CLASS }}} DECLARE_DYNAMIC, IMPLEMENT_DYNAMIC 매크로는 CObject에서 상속받은 클래스에 RTCI(RunTime Class Information) 혹은 Runtime Information 기능을 추가한다. DECLARE_DYNAMIC, IMPLEMENT_DYNAMIC을 통해 아래와 같은 멤버가 선언, 구현된다. {{{#!gcode public: static const CRuntimeClass classCChild; virtual CRuntimeClass* GetRuntimeClass() const; }}} 이제 실행 시 GetRuntimeClass()라는 함수를 통해 객체의 정보를 얻을 수 있는 기능을 가지게 된다. GetRuntimeClass()는 다음처럼 구현되어 있고 {{{#!gcode CRuntimeClass* CChild::GetRuntimeClass() const { return RUNTIME_CLASS(CChild); } }}} 마지막으로 RUNTIME_CLASS() 매크로는 다음과 같이 선언되어 있다. {{{#!gcode #define _RUNTIME_CLASS(class_name) ((CRuntimeClass*)(&class_name::class##class_name)) #ifdef _AFXDLL #define RUNTIME_CLASS(class_name) (class_name::GetThisClass()) #else #define RUNTIME_CLASS(class_name) _RUNTIME_CLASS(class_name) #endif }}} 즉, 이 매크로는 매개변수로 넘어온 class_name 클래스의 정적 CRuntimeClass 멤버의 주소로 변형된다. {{{+1 MFC 메시지 전달 순서 }}} CView -> CDocument -> CDocTemplate ->CFrameWnd -> CWinApp {{{+1 MFC 메시지 맵이 필요한 이유 }}} MFC 메시지 맵이 이런 구조로 만들어진 이유가 있다. 상속 관계에 있는 클래스는 자식 클래스의 메시지부터 거슬러 올라가기 때문에 메시지 핸들러가 가상 함수에 오버라이딩 되어야 할 것 같이 느껴진다. 그런데 MFC 클래스는 메시지 핸들러 함수가 가상 함수로 정의되어 있지 않다. 동적 바인딩을 하기 위해선 클래스에 4바이트(32비트 윈도우의 경우) 메모리 주소를 저장할 공간이 더 필요하다(동적 바인딩 참조). 그런데 만약 모든 메시지 핸들러 함수가 가상 함수로 되어 있다면 메시지 핸들러가 대략 200여개 있으니 윈도우 클래스마다 대략 800바이트 이상이 더 필요하게 된다. 프로그램이 하나 실행되면 뷰, 툴바, 각종 컨트롤 등 기본적으로 수 개~수십 개의 윈도우가 생기는데 당연히 이건 대단한 자원 낭비다. 메시지 맵은 매크로를 이용해 message map entry macro에 등록한 함수만 가상 함수처럼 동적 바인딩을 해준다. AFX의 파생 클래스마다 모두 이런 골격을 만들어 뒀기에 가능하다. 개발자는 사용할 윈도우 메시지 엔트리를 등록하고 처리할 함수를 만들면 모든 일이 끝난다. 참고 ---- https://msdn.microsoft.com/en-us/library/shfzay75.aspx http://msdn.microsoft.com/en-us/library/0x0cx6b1.aspx http://www.codeproject.com/Articles/598/Windows-Message-Handling-Part