MFC 메시지 맵의 구현과 동작 방식
메시지 맵 Message Maps (MFC)
윈도우 프로그램을 만들 때 메시지가 오고가는 것을 제어하고 싶을 때, Win32 API 에서는 GetMessage()를 이용해 TranslateMessage()와 DispatchMessage() 를 이용해 윈도우 메시지를 제어했다. MFC에서 Win32 처럼 메시지를 받고 메시지를 처리하는 것이 메시지 맵이다.
윈도우 프로그램을 만들 때 메시지가 오고가는 것을 제어하고 싶을 때, Win32 API 에서는 GetMessage()를 이용해 TranslateMessage()와 DispatchMessage() 를 이용해 윈도우 메시지를 제어했다. MFC에서 Win32 처럼 메시지를 받고 메시지를 처리하는 것이 메시지 맵이다.
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 순서로 메시지가 전달된다.
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 순서로 메시지가 전달된다.
CCmdTarget
대부분의 메시지는 프로그램에 대한 사용자의 반응이다. 커맨드는 메뉴 아이콘 또는 툴바 버튼 마우스 클릭이나 키 입력에 의해 생성된다. 사용자는 또한 윈도우 이동이나 리사이징 등으로 윈도우 메시지를 생성할 수도 있다. CCmdTarget 클래스는 커맨드 메시지들을 수신하고 응답할 수 있도록 도와준다. 그렇기 때문에 이런 기능을 필요로 하는 클래스들은 이 클래스를 상속하면 된다. CCmdTarget 클래스는 메시지 아이디를 그에 대응하는 커맨드 핸들러에 매핑해주는 메시지맵 인터페이스를 갖고 있다. 이 메시지맵 인터페이스를 통해서 커맨드 메시지는 이 메시지의 핸들러를 제공하는 클래스들을 순회하게 된다.
대부분의 메시지는 프로그램에 대한 사용자의 반응이다. 커맨드는 메뉴 아이콘 또는 툴바 버튼 마우스 클릭이나 키 입력에 의해 생성된다. 사용자는 또한 윈도우 이동이나 리사이징 등으로 윈도우 메시지를 생성할 수도 있다. CCmdTarget 클래스는 커맨드 메시지들을 수신하고 응답할 수 있도록 도와준다. 그렇기 때문에 이런 기능을 필요로 하는 클래스들은 이 클래스를 상속하면 된다. CCmdTarget 클래스는 메시지 아이디를 그에 대응하는 커맨드 핸들러에 매핑해주는 메시지맵 인터페이스를 갖고 있다. 이 메시지맵 인터페이스를 통해서 커맨드 메시지는 이 메시지의 핸들러를 제공하는 클래스들을 순회하게 된다.
struct AFX_MSGMAP
실질적인 메시지 맵을 표현하는 구조체로 부모 클래스의 메시지 맵을 구하는 함수 포인터와 현재 클래스의 메시지 맵 엔트리 리스트 가지고 있다.
MFC는 각 클래스 별로 메시지 맵을 연결 리스트로 유지한다. 현재 클래스의 메시지 맵에 명령에 해당하는 메시지 맵 엔트리가 없을 경우 부모 클래스 메시지 맵에서 해당하는 메시지 맵 엔트리가 있는지 검사한다.
MFC는 각 클래스 별로 메시지 맵을 연결 리스트로 유지한다. 현재 클래스의 메시지 맵에 명령에 해당하는 메시지 맵 엔트리가 없을 경우 부모 클래스 메시지 맵에서 해당하는 메시지 맵 엔트리가 있는지 검사한다.
struct AFX_MSGMAP_ENTRY
메시지 맵에서 윈도우 메시지와 메시지를 처리하는 함수를 실질적으로 맵핑하는 구조체이다.
DECLARE_MESSAGE_MAP()
헤더에서 메시지 맵을 선언한다. 클래스 선언의 마지막에 사용하거나 DECLARE_MESSAGE_MAP()을 사용한 이후에 멤버를 선언할 경우 반드시 액세스 타입(private, public)을 새로 지정해줘야 한다.
헤더에서 메시지 맵을 선언한다. 클래스 선언의 마지막에 사용하거나 DECLARE_MESSAGE_MAP()을 사용한 이후에 멤버를 선언할 경우 반드시 액세스 타입(private, public)을 새로 지정해줘야 한다.
BEGIN_MESSAGE_MAP(), END_MESSAGE_MAP()
메시지 맵 구현 코드를 생성한다. 해당 클래스의 소스(.cpp)에 아래와 같은 형식으로 작성된다.
메시지 맵 구현 코드를 생성한다. 해당 클래스의 소스(.cpp)에 아래와 같은 형식으로 작성된다.
매크로에 의해 현재 클래스의 메시지 맵 참조로 확장된다.
즉, 다음 같이 변경된다.
ON_WM_PAINT() 같은 message map entry macro들은 afxmsg_h. 파일에 선언되어 있다. 예를 들어 ON_WM_PAINT() 매크로는 다음과 같이 선언되어 있다.
DECLARE_DYNAMIC, IMPLEMENT_DYNAMIC, RUNTIME_CLASS
DECLARE_DYNAMIC, IMPLEMENT_DYNAMIC 매크로는 CObject에서 상속받은 클래스에 RTCI(RunTime Class Information) 혹은 Runtime Information 기능을 추가한다.
DECLARE_DYNAMIC, IMPLEMENT_DYNAMIC 매크로는 CObject에서 상속받은 클래스에 RTCI(RunTime Class Information) 혹은 Runtime Information 기능을 추가한다.
DECLARE_DYNAMIC, IMPLEMENT_DYNAMIC을 통해 아래와 같은 멤버가 선언, 구현된다.
이제 실행 시 GetRuntimeClass()라는 함수를 통해 객체의 정보를 얻을 수 있는 기능을 가지게 된다.
GetRuntimeClass()는 다음처럼 구현되어 있고
마지막으로 RUNTIME_CLASS() 매크로는 다음과 같이 선언되어 있다.
즉, 이 매크로는 매개변수로 넘어온 class_name 클래스의 정적 CRuntimeClass 멤버의 주소로 변형된다.
MFC 메시지 전달 순서
CView -> CDocument -> CDocTemplate ->CFrameWnd -> CWinApp
MFC 메시지 맵이 필요한 이유
MFC 메시지 맵이 이런 구조로 만들어진 이유가 있다. 상속 관계에 있는 클래스는 자식 클래스의 메시지부터 거슬러 올라가기 때문에 메시지 핸들러가 가상 함수에 오버라이딩 되어야 할 것 같이 느껴진다. 그런데 MFC 클래스는 메시지 핸들러 함수가 가상 함수로 정의되어 있지 않다. 동적 바인딩을 하기 위해선 클래스에 4바이트(32비트 윈도우의 경우) 메모리 주소를 저장할 공간이 더 필요하다(동적 바인딩 참조). 그런데 만약 모든 메시지 핸들러 함수가 가상 함수로 되어 있다면 메시지 핸들러가 대략 200여개 있으니 윈도우 클래스마다 대략 800바이트 이상이 더 필요하게 된다. 프로그램이 하나 실행되면 뷰, 툴바, 각종 컨트롤 등 기본적으로 수 개~수십 개의 윈도우가 생기는데 당연히 이건 대단한 자원 낭비다.
MFC 메시지 맵이 이런 구조로 만들어진 이유가 있다. 상속 관계에 있는 클래스는 자식 클래스의 메시지부터 거슬러 올라가기 때문에 메시지 핸들러가 가상 함수에 오버라이딩 되어야 할 것 같이 느껴진다. 그런데 MFC 클래스는 메시지 핸들러 함수가 가상 함수로 정의되어 있지 않다. 동적 바인딩을 하기 위해선 클래스에 4바이트(32비트 윈도우의 경우) 메모리 주소를 저장할 공간이 더 필요하다(동적 바인딩 참조). 그런데 만약 모든 메시지 핸들러 함수가 가상 함수로 되어 있다면 메시지 핸들러가 대략 200여개 있으니 윈도우 클래스마다 대략 800바이트 이상이 더 필요하게 된다. 프로그램이 하나 실행되면 뷰, 툴바, 각종 컨트롤 등 기본적으로 수 개~수십 개의 윈도우가 생기는데 당연히 이건 대단한 자원 낭비다.
메시지 맵은 매크로를 이용해 message map entry macro에 등록한 함수만 가상 함수처럼 동적 바인딩을 해준다. AFX의 파생 클래스마다 모두 이런 골격을 만들어 뒀기에 가능하다. 개발자는 사용할 윈도우 메시지 엔트리를 등록하고 처리할 함수를 만들면 모든 일이 끝난다.
참고