메시지 맵, 핸들러, 매크로의 관계

 

참고문헌 : 1. 마이크로소프트웨어 (1997년 1월호) MFC의 메시지 처리, 매크로가 해결한다

                2. Visual C++ 6 완벽가이드 (영진출판사, p157~170)

 

  메시지맵은 일종의 매크로인데, 파생클래스의 메시지 핸들러 함수를 여기에 등록하면 기반 클래스의 함수를 무시하고, 파생클래스의 함수를 호출해 줍니다. 즉 메시지맵에 등록된 함수에 대해서만 가상함수처럼 바인딩을 해주는 것입니다.

 그리고 메시지맵은 BEGIN_MESSAGE_MAP 이라는 매크로로 시작해서, END_MESSAGE_MAP 이라는 매크로로 끝납니다. 참고로 알아보자면 이 매크로는 afxwin.h 파일에 다음처럼 정의되어 있습니다. 꽤 복잡하네요.

#ifdef _AFXDLL
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
        const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() \
                { return &baseClass::messageMap; } \
        const AFX_MSGMAP* theClass::GetMessageMap() const \
                { return &theClass::messageMap; } \
        AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \
        { &theClass::_GetBaseMessageMap, &theClass::_messageEntries[0] }; \
        AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \
        { \

#else
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
        const AFX_MSGMAP* theClass::GetMessageMap() const \
                { return &theClass::messageMap; } \
        AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \
        { &baseClass::messageMap, &theClass::_messageEntries[0] }; \
        AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \
        { \

#endif

#define END_MESSAGE_MAP() \
                {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
        }; \

 

복잡한 이론은 놔두기로 하고, 실제 코드를 보면서 설명을 하죠.

1. 다이얼로그 베이스로 프로젝트를 만듭니다. 다음은 프로젝트를 만든 후 아무런 작업을 하지 않은 초기상태의 다이얼로그의 헤더파일 일부입니다.

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

// Dialog Data
        //{{AFX_DATA(CMessageMapDlg)
        enum { IDD = IDD_MESSAGEMAP_DIALOG };
                // NOTE: the ClassWizard will add data members here
        //}}AFX_DATA

        // ClassWizard generated virtual function overrides
        //{{AFX_VIRTUAL(CMessageMapDlg)
        protected:
        virtual void DoDataExchange(CDataExchange* pDX);        // DDX/DDV support
        //}}AFX_VIRTUAL

// Implementation
protected:
        HICON m_hIcon;

        // Generated message map functions
        //{{AFX_MSG(CMessageMapDlg)
        virtual BOOL OnInitDialog();
        afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
        afx_msg void OnPaint();
        afx_msg HCURSOR OnQueryDragIcon();
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
};

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

 

2. 다음은 역시 초기상태의 cpp 파일의 일부입니다.

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

        // ClassWizard generated virtual function overrides
        //{{AFX_VIRTUAL(CAboutDlg)
        protected:
        virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
        //}}AFX_VIRTUAL

// Implementation
protected:
        //{{AFX_MSG(CAboutDlg)
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
};

CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)
{
        //{{AFX_DATA_INIT(CAboutDlg)
        //}}AFX_DATA_INIT
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
        CDialog::DoDataExchange(pDX);
        //{{AFX_DATA_MAP(CAboutDlg)
        //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
        //{{AFX_MSG_MAP(CAboutDlg)
                // No message handlers
        //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CMessageMapDlg dialog

CMessageMapDlg::CMessageMapDlg(CWnd* pParent /*=NULL*/)
        : CDialog(CMessageMapDlg::IDD, pParent)
{
        ...........................................
}

void CMessageMapDlg::DoDataExchange(CDataExchange* pDX)
{
        CDialog::DoDataExchange(pDX);
        //{{AFX_DATA_MAP(CMessageMapDlg)
                // NOTE: the ClassWizard will add DDX and DDV calls here
        //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CMessageMapDlg, CDialog)
        //{{AFX_MSG_MAP(CMessageMapDlg)
        ON_WM_SYSCOMMAND()
        ON_WM_PAINT()
        ON_WM_QUERYDRAGICON()
        //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CMessageMapDlg message handlers

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

 

3. 다이얼로그 템플릿에 버튼을 5개 만든 후, 각각의 버튼에 대해 핸들러를 만듭니다.

다음은 그 후의 헤더파일입니다. 화살표 부분이 바뀐 부분입니다.

 

// Dialog Data
        //{{AFX_DATA(CMessageMapDlg)
        enum { IDD = IDD_MESSAGEMAP_DIALOG };
                // NOTE: the ClassWizard will add data members here
        //}}AFX_DATA

        // ClassWizard generated virtual function overrides
        //{{AFX_VIRTUAL(CMessageMapDlg)
        protected:
        virtual void DoDataExchange(CDataExchange* pDX);        // DDX/DDV support
        //}}AFX_VIRTUAL

// Implementation
protected:
        HICON m_hIcon;

        // Generated message map functions
        //{{AFX_MSG(CMessageMapDlg)
        virtual BOOL OnInitDialog();
        afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
        afx_msg void OnPaint();
        afx_msg HCURSOR OnQueryDragIcon();
        afx_msg void OnButton1();    // <-----
        afx_msg void OnButton2();    // <-----
        afx_msg void OnButton3();    // <-----
        afx_msg void OnButton4();    // <-----
        afx_msg void OnButton5();    // <-----
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
};

 

4. cpp 파일입니다. 역시 여기도 바뀐 부분을 화살표로 표시했습니다.

 

        // ClassWizard generated virtual function overrides
        //{{AFX_VIRTUAL(CAboutDlg)
        protected:
        virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
        //}}AFX_VIRTUAL

// Implementation
protected:
        //{{AFX_MSG(CAboutDlg)
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
};

CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)
{
        //{{AFX_DATA_INIT(CAboutDlg)
        //}}AFX_DATA_INIT
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
        CDialog::DoDataExchange(pDX);
        //{{AFX_DATA_MAP(CAboutDlg)
        //}}AFX_DATA_MAP
}
 
BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
        //{{AFX_MSG_MAP(CAboutDlg)
                // No message handlers
        //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CMessageMapDlg dialog
 
CMessageMapDlg::CMessageMapDlg(CWnd* pParent /*=NULL*/)
        : CDialog(CMessageMapDlg::IDD, pParent)
{
       ....................................................
}

void CMessageMapDlg::DoDataExchange(CDataExchange* pDX)
{
        CDialog::DoDataExchange(pDX);

        //{{AFX_DATA_MAP(CMessageMapDlg)
                // NOTE: the ClassWizard will add DDX and DDV calls here
        //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CMessageMapDlg, CDialog)
        //{{AFX_MSG_MAP(CMessageMapDlg)
        ON_WM_SYSCOMMAND()
        ON_WM_PAINT()
        ON_WM_QUERYDRAGICON()
        ON_BN_CLICKED(IDC_BUTTON1, OnButton1)   // <-----
        ON_BN_CLICKED(IDC_BUTTON2, OnButton2)   // <-----
        ON_BN_CLICKED(IDC_BUTTON3, OnButton3)   // <-----
        ON_BN_CLICKED(IDC_BUTTON4, OnButton4)   // <-----
        ON_BN_CLICKED(IDC_BUTTON5, OnButton5)   // <-----
        //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CMessageMapDlg message handlers

void CMessageMapDlg::OnButton1()
{
        // TODO: Add your control notification handler code here
}

void CMessageMapDlg::OnButton2()
{
        // TODO: Add your control notification handler code here
}

void CMessageMapDlg::OnButton3()
{
        // TODO: Add your control notification handler code here
}

void CMessageMapDlg::OnButton4()
{
        // TODO: Add your control notification handler code here
}

void CMessageMapDlg::OnButton5()
{
        // TODO: Add your control notification handler code here
}

 

5. 위에서 버튼 5개를 추가한 다음에 헤더파일의 바뀐 부분을 살펴보면 다음과 같습니다.

 헤더파일의 경우, 핸들러 함수의 선언이 포함됩니다. 다음처럼 말이죠.

        afx_msg void OnButton1();    
        afx_msg void OnButton2();    
        afx_msg void OnButton3();    
        afx_msg void OnButton4();   
        afx_msg void OnButton5();   

여기서 보면 함수의 앞부분에 afx_msg 라는 것이 보이죠. 이건 afxwin.h 파일에 다음과 같이 define 되어 있습니다.

   #ifndef afx_msg
   #define afx_msg         // intentional placeholder
   #endif

 define 문이 전개되어 코드가 치환되면 그냥 없어집니다. 있는거나 없는거나 마찬가지라는 말이죠. 사실 안붙여도 상관없지만 핸들러 함수라는 것을 표시하기 위한 겁니다.

 

6. CPP파일의 추가된 부분은 다음과 같습니다. 다음과 같은 매크로 외에도 내용이 없는 핸들러 함수가 추가됩니다. 핸들러 함수의 내용은 프로그래머가 직접 채워야 하겠죠.

        ON_BN_CLICKED(IDC_BUTTON1, OnButton1)   
        ON_BN_CLICKED(IDC_BUTTON2, OnButton2)   
        ON_BN_CLICKED(IDC_BUTTON3, OnButton3)   
        ON_BN_CLICKED(IDC_BUTTON4, OnButton4)  
        ON_BN_CLICKED(IDC_BUTTON5, OnButton5)  

 이 부분은 버튼의 ID와 핸들러 함수를 연결시키는 매크로입니다. ON_BN_CLICKED 부분이 매크로구요, IDC_BUTTON1 부분은 버튼의 ID , OnButton1은 그 버튼에 대한 핸들러 함수이죠. 버튼을 5개 놓았으므로 연결시키는 매크로 부분이 5개가 됩니다.

 

7. 만약 버튼이 100개라면 위와 같은 매크로가 100개가 만들어져야 하고, 그에 따라 핸들러 함수도 100개가 만들어지겠죠. 상당한 노가다가 아닐 수 없습니다. 이걸 좀더 단순하게 줄이기 위해서 다른 매크로를 사용하면 됩니다. ON_CONTROL_EX 나 ON_CONTROL_RANGE 같은 매크로 말이죠.

 이것에 대해 알아보기 전에 다음내용부터 먼저 알아보기로 하죠. 다음 방법을 사용해서 여러개의 핸들러 함수를 하나로 줄일 수 있습니다.

                            ---- X ---- X ---- X ----

7.1 ON_BN_CLICKED  매크로는 afxmsg_.h 파일을 보면 다음처럼 정의되어 있습니다.

     #define ON_BN_CLICKED(id, memberFxn) \
             ON_CONTROL(BN_CLICKED, id, memberFxn)

 ON_BN_CLICKED 의 경우 ON_CONTROL의 다른 이름이죠. 이 매크로의 확장판으로 ON_CONTROL_EX 와 ON_CONTROL_RANGE 라는게 있습니다.

 ON_CONTROL_EX 는 사실 MFC에서 제공되지 않는 건데요. 없으면 만들면 됩니다. 다음처럼 말이죠.

   #define ON_CONTROL_EX(wNotifyCode, id, memberFxn)    \
      { WM_COMMAND, (WORD)wNotifyCode, (WORD)id, (WORD)id, AfxSig_bw,   \
       (AFX_PMSG)(BOOL (AFX_MSG_CALL CCmdTarget::*)(UINT))&memberFxn },

 이 매크로는 다음과 같이 사용할 수 있습니다.

cpp 파일에서 ON_BN_CLICKED 부분을 ON_CONTROL_EX 매크로로 바꾸어보죠.

매크로맵 부분을 다음처럼 바꾸면 됩니다.

 

BEGIN_MESSAGE_MAP(CMessageMapDlg, CDialog)
        //{{AFX_MSG_MAP(CMessageMapDlg)
        ON_WM_SYSCOMMAND()
        ON_WM_PAINT()
        ON_WM_QUERYDRAGICON()

//      ON_BN_CLICKED(IDC_BUTTON1, OnButton1)     // <--- 원래의 매크로
//      ON_BN_CLICKED(IDC_BUTTON2, OnButton2)     // <--- 원래의 매크로
//      ON_BN_CLICKED(IDC_BUTTON3, OnButton3)     // <--- 원래의 매크로
//      ON_BN_CLICKED(IDC_BUTTON4, OnButton4)    // <--- 원래의 매크로
//      ON_BN_CLICKED(IDC_BUTTON5, OnButton5)    // <--- 원래의 매크로

        //}}AFX_MSG_MAP

        ON_CONTROL_EX(BN_CLICKED, IDC_BUTTON1, OnButton1)  // <--- 바뀐 매크로
        ON_CONTROL_EX(BN_CLICKED, IDC_BUTTON2, OnButton1)  // <--- 바뀐 매크로
        ON_CONTROL_EX(BN_CLICKED, IDC_BUTTON3, OnButton1)  // <--- 바뀐 매크로
        ON_CONTROL_EX(BN_CLICKED, IDC_BUTTON4, OnButton1)  // <--- 바뀐 매크로
        ON_CONTROL_EX(BN_CLICKED, IDC_BUTTON5, OnButton1)  // <--- 바뀐 매크로

END_MESSAGE_MAP()

 

 7.2 여기서 바뀐 부분이 무언지 이해가 되나요?

바로 핸들러 함수가 하나로 줄었다는 겁니다. 원래의 매크로를 사용했다면 버튼 5개에 대해 각각 하나씩, 5개의 핸들러 함수가 사용되었지만, 바뀐 매크로를 사용한 후 핸들러 함수가 하나로 줄었습니다.

 이 경우 문제가 하나 발생하겠죠. 바로 어느 버튼이 눌렸는지(즉 눌린 버튼의 ID 가 무엇인지) 확인할 방법이 없다는 거죠. 이 문제는 다음과 같이 해결하면 됩니다.

핸들러 함수를 다음과 같이 구성합니다.

 

void CMessageMapDlg::OnButton1()
{
     const MSG *pMsg = GetCurrentMessage();

     // WM_COMMAND에서는 wParam이 컨트롤의 ID
     switch(pMsg->wParam) {
          .....
      }
}

 이렇게 해서 위의 문제는 해결되었습니다.

 

7.3 그리고 한가지 더 주목할 게 있습니다. 매크로가 들어가는 위치인데요. 메시지 맵은 다음 두 문장 사이에 들어갑니다. 클래스위저드가 만든 것과 사용자가 만든 것 모두 말이죠.

BEGIN_MESSAGE_MAP(CMessageMapDlg, CDialog)
...
END_MESSAGE_MAP()

그런데, 이 두 문장 안에 또 다른 문장이 있습니다.

        //{{AFX_MSG_MAP(CMessageMapDlg)
         ...........
        //}}AFX_MSG_MAP

 형태상으로는 그냥 일반 주석문처럼 보이는데요. 이것은 바로 클래스 위저드가 만들어주는 구문이 들어가는 부분입니다. 사용자가 만든 구문은 이 구문 바깥쪽에 써주어야 합니다.

그러니까 위치는 다음부분이죠.

BEGIN_MESSAGE_MAP(CMessageMapDlg, CDialog)
        //{{AFX_MSG_MAP(CMessageMapDlg)
}
       //}}AFX_MSG_MAP
           //  <--------- 여기가 사용자가 작성한 매크로가 들어가는 곳
END_MESSAGE_MAP()

 

하지만 가장 큰 문제가 하나 남아있는데요. 바로 Release 모드로 컴파일할 경우 실행시 에러가 난다는 건데요. Debug 모드에선 전혀 문제가 없는데 말이죠. 그래서 다음과 같은 방법으로 해결이 가능합니다.

헤더파일에서 핸들러 함수의 프로토타입 부분을 다음처럼 고칩니다.

 

        afx_msg void OnButtonFxn(UINT nID);

cpp 파일에서는 핸들러 함수의 앞부분을 고칩니다.

void CMessageMapDlg::OnButtonFxn(UINT nID)
{
      ....................
}

 

 고칠내용이 무어냐 하면, 핸들러 함수에 인자를 하나 주는 겁니다. 인자 자체는 별 의미가 없습니다. 그저 인자를 주면 신기하게도 Release 모드에서 에러가 나지 않더라는 것이죠.

 그리고 다른 방법으로도 해결이 가능합니다. 그 해결책은 ON_CONTROL_EX 를 사용하지 않는 겁니다. 하하... 사실 더 간단한 방법이 있긴 합니다. ON_BN_CLICKED 매크로를 다음처럼 고치는 것이죠.

      ON_BN_CLICKED(IDC_BUTTON1, OnButton1)
      ON_BN_CLICKED(IDC_BUTTON2, OnButton1)
      ON_BN_CLICKED(IDC_BUTTON3, OnButton1)
      ON_BN_CLICKED(IDC_BUTTON4, OnButton1)
      ON_BN_CLICKED(IDC_BUTTON5, OnButton1)

이렇게 해서 간단하게 해결되었습니다. 보시다시피 ON_BN_CLICKED 매크로에서 핸들러 함수 부분인 OnButton1 부분만 하나로 통일해 주는 것 뿐입니다.

이러한 방법을 사용해서 핸들러 함수를 하나로 줄일 수 있습니다.

 

8. 위에서는 핸들러 함수를 하나로 줄여 보았습니다. 하지만 매크로 부분은 역시 버튼 개수만큼 작성해야 합니다. 물론 매크로 부분은 클래스 위저드가 만들어 주는 것이고, 사용자가 해야 할 부분은 클래스 위저드가 만들어주는 매크로 부분을 조금 고쳐주는 것이죠.

 그런데 ON_CONTROL_RANGE 라는 매크로를 이용해서 다음과 같이 매크로 부분도 하나로 줄여줄 수 있습니다.

     ON_CONTROL_RANGE(BN_CLICKED, IDC_BUTTON1, IDC_BUTTON5, OnButton1)

 이 매크로도 역시 Debug 모드에서는 멀쩡하고, Release 모드에서 실행에러가 납니다. 그래서 앞에서 말했듯이, 이 에러를 피하기 위해 핸들러 함수에 인자를 하나 추가해 줍니다. 그렇게 하면 신기하게도 에러가 없어집니다.

 

- end of this article -