4.메시지 맵

.메시지 처리

이전 버전의 WTL은 메시지 핸들러를 작성하는데 대한 지원이 없어 수작업으로 핸들러를 추가해야 했다. 그러나 WTL 8.0부터는 클래스 뷰와 속성창의 메시지 페이지를 통해 핸들러를 자동으로 작성하는 기능이 지원되어 훨씬 더 편리해졌다. 한번씩만 실습해 보면 어렵지 않으므로 예제를 만들어 보자.

 

: Messagehandler

먼저 메시지를 처리할 클래스를 클래스 뷰에서 선택한다. 그러면 속성창에 선택된 클래스의 속성이 나타나는데 상단의 버튼을 눌러 메시지 페이지로 전환하면 발생 가능한 메시지에 대한 목록이 나타난다. 여기서 처리하고자 하는 메시지를 선택하면 된다. 예를 들어 뷰에서 WM_CREATE 메시지를 처리하고 싶다고 해 보자.

 

클래스 뷰에서 CMessageHandlerView 클래스를 선택하고 속성창의 메시지 페이지를 열어 보면 뷰에서 발생할 수 있는 메시지가 나열되며 이미 핸들러가 작성되어 있는 메시지에 대해서는 오른쪽에 핸들러의 이름으 표시된다. WM_CREATE의 콤보 박스에서 <추가>를 선택하면 이 메시지를 처리하기 위한 모든 조치가 자동으로 수행된다. 먼저 헤더 파일의 메시지 맵에 엔트리가 추가된다. WM_PAINT 핸들러는 마법사가 미리 만들어 놓은 것이다.

 

     BEGIN_MSG_MAP(CMessageHandlerView)

          MESSAGE_HANDLER(WM_PAINT, OnPaint)

          MESSAGE_HANDLER(WM_CREATE, OnCreate)

     END_MSG_MAP()

 

이 엔트리에 의해 WM_CREATE 메시지에 대해 OnCreate 함수가 호출되는 것이다. 헤더 파일에 OnCreate 함수의 원형이 선언된다. 메시지 ID와 파라미터, 메시지 처리 여부를 리턴하는 참조 인수가 전달된다.

 

     LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);

 

그리고 구현 파일에는 핸들러 함수의 본체가 생성된다. 형식 인수들이 주석 처리되어 있는데 이중 사용하고 싶은 것만 주석을 풀어서 사용하면 된다. 왜 형식 인수들을 주석으로 묶어 놓는가 하면 인수를 쓰지 않으면 불필요한 경고가 발생하기 때문이다. 본체 내부는 비어 있고 코드 작성 지침이 주석문으로만 표기되어 있다. 주석문을 지우고 이 자리에 원하는 코드를 작성한다.

 

LRESULT CMessageHandlerView::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     // TODO: 여기에 메시지 처리기 코드를 추가 /또는 기본값을 호출합니다.

 

     return 0;

}

 

그럼, 간단하게 시계를 만들어 보자. 타이머 설치 및 해제, 타이머 메시지 처리, 시간 출력 등을 위해 4개의 핸들러를 작성해야 한다. 먼저 헤더 파일에 현재 시간을 저장할 문자열을 선언한다. CString은 atlmisc.h에 정의되어 있으므로 이 파일을 먼저 인클루드해야 한다.

 

#include <atlmisc.h>

class CMessageHandlerView : public CWindowImpl<CMessageHandlerView>

{

public:

     CString sTime;

 

구현 파일에 WM_DESTROY와 WM_TIMER에 대해 2개의 핸들러를 더 생성하고 다음 코드를 작성한다. 코드양이 많아 보이지만 대부분 마법사가 자동으로 생성한 것이므로 실제로 입력해야 할 코드는 얼마 되지 않는다.

 

LRESULT CMessageHandlerView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CPaintDC dc(m_hWnd);

 

     dc.TextOut(10,10,sTime);

 

     return 0;

}

 

LRESULT CMessageHandlerView::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     SetTimer(1,1000);

 

     return 0;

}

 

LRESULT CMessageHandlerView::OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     KillTimer(1);

 

     return 0;

}

 

LRESULT CMessageHandlerView::OnTimer(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     SYSTEMTIME st;

 

     switch (wParam) {

     case 1:

          GetLocalTime(&st);

          sTime.Format(TEXT("%d:%d:%d"),st.wHour,st.wMinute,st.wSecond);

          Invalidate();

          break;

     }

 

     return 0;

}

 

WM_CREATE에서 1번 타이머를 설치하고 WM_DESTROY에서 해제한다. 이 두 메시지는 인수로 특별히 전달되는 내용이 없으므로 인수를 사용하지 않았다. OnTimer 핸들러는 어떤 타이머로부터 메시지가 발생했는지를 wParam을 통해 읽어야 하므로 wParam을 주석 해제했다. GetLocalTime 함수로 현재 시간을 구해 sTime 문자열에 대입해 놓으면 OnPaint에서 이 문자열을 화면으로 출력한다.

매 초마다 시간이 갱신되므로 시계가 1초 단위로 갱신될 것이다. 보다시피 새벽에 잠도 못자고 강좌를 쓰고 있는 중이다. 다음은 마우스 왼쪽 버튼을 누를 때 누른 위치에 "Click"이라는 문자열을 출력해 보자. WM_LBUTTONDOWN 메시지에 대한 핸들러를 만들고 다음 코드를 작성한다.

 

LRESULT CMessageHandlerView::OnLButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/)

{

     CClientDC dc(m_hWnd);

    

     dc.TextOut(LOWORD(lParam),HIWORD(lParam),TEXT("Click"));

 

     return 0;

}

 

클릭한 위치의 좌표가 lParam으로 전달되므로 lParam의 주석을 풀고 상하위 워드를 분리하여 좌표를 얻는다. 이 좌표에 "Click"이라는 문자열을 출력했다. wParam으로는 조합키의 상태가 전달되는데 이 예제에서는 당장 사용하지 않으므로 주석을 풀지 않았다. 마지막 인수 bHandled는 메시지를 처리했는가 아닌가를 리턴하는 참조 인수이다. 디폴트는 TRUE이지만 FALSE로 변경하면 처리하지 않은 것으로 전달되어 운영체제에 의해 디폴트 처리된다.

그런데 uMsg 인수는 왜 전달되는 것일까? 각 핸들러별로 전담 메시지가 정해져 있으므로 굳이 메시지 ID를 인수로 전달할 필요가 없어 보인다. 과연 그렇기는 하다. OnTimer가 처리하는 메시지는 WM_TIMER이고 OnPaint가 처리하는 메시지는 항상 OnPaint이다. 그러나 가끔 메시지 핸들러 하나가 여러 개의 메시지를 통합 처리하는 경우도 있으므로 메시지 ID가 원형에 포함되어 있다.

예를 들어 WM_KEYDOWN과 WM_KEYUP을 OnKeyInput이라는 함수 하나가 모두 받을 수도 있으며 이 경우 어떤 메시지에 의해 함수가 호출되었는지 uMsg 인수로부터 판별할 수 있다. 그런 경우가 아니라면 uMsg 인수는 굳이 사용하지 않아도 상관없다.

.메시지 크래커

속성창을 통해 메시지 핸들러를 쉽게 추가할 수 있다는 면에서 편리하며 MFC와 같은 방법이라 익숙하기도 하다. 그러나 MFC보다는 역시 한 수 아래인데 메시지와 함수를 연결만 해 줄뿐 메시지의 인수를 분리해 주지는 않는다. 마우스 좌표를 정수형 두 개나 Point 객체로 전달하는 것이 아니라 lParam으로 전달하므로 직접 분리해서 사용해야 하는 불편함이 있다. 메시지의 인수 의미를 모두 외우고 다닐 수는 없으므로 필요할 때마다 MSDN을 참조해야 한다.

WTL에는 메시지를 분리해 주는 매크로도 정의되어 있기는 하지만 개발툴이 직접적으로 지원하지 않으므로 수작업으로 메시지 맵을 편집해야 한다. 구조적으로는 이 방법이 더 편리하지만 개발툴의 지원이 없어 오히려 더 불편하다. 앞 예제와 똑같은 예제를 메시지 크래커로 작성해 보자.

 

: MessageCracker

뷰의 헤더 파일을 열어 다음 소스를 입력한다. 개발툴의 지원이 거의 없으므로 모조리 손으로 다 직접 입력해야 한다.

 

#include <atlmisc.h>

#include <atlcrack.h>

 

class CMessageCrackerView : public CWindowImpl<CMessageCrackerView>

{

public:

     DECLARE_WND_CLASS(NULL)

     CString sTime;

 

     BOOL PreTranslateMessage(MSG* pMsg);

 

     BEGIN_MSG_MAP_EX(CMessageCrackerView)

          MESSAGE_HANDLER(WM_PAINT, OnPaint)

          MSG_WM_CREATE(OnCreate)

          MSG_WM_DESTROY(OnDestroy)

          MSG_WM_TIMER(OnTimer)

          MSG_WM_LBUTTONDOWN(OnLButtonDown);

     END_MSG_MAP()

 

     LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);

     int OnCreate(LPCREATESTRUCT lpCreateStruct);

     void OnDestroy();

     void OnTimer(UINT_PTR nIDEvent);

     void OnLButtonDown(UINT nFlags, CPoint point);

};

 

메시지 크래커는 atlcrack.h에 의해 제공되므로 먼저 이 헤더 파일을 인클루드한다. 그리고 메시지 맵의 시작 매크로를 BEGIN_MSG_MAP_EX로 변경한다. 단순 버전에 비해 뒤에 _EX가 더 붙어 있다. 마법사가 만들어 놓은 OnPaint는 그대로 두고 새로 만드는 OnCreate부터는 MSG_WM_XXX 식으로 XXX자리에 처리하고자 하는 메시지 이름을 쓰고 괄호안에 이 메시지를 받을 핸들러 함수를 쓰면 된다.

핸들러 함수의 원형은 아래쪽에 선언해 두었다. 메시지의 종류에 따라 받아들이는 인수가 다르므로 핸들러의 원형도 메시지마다 다르다. 구현 파일에는 각 핸들러 함수의 본체를 작성한다. 형식이 조금 다를 뿐 앞 예제와 내용은 같다.

 

LRESULT CMessageCrackerView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CPaintDC dc(m_hWnd);

 

     dc.TextOut(10,10,sTime);

 

     return 0;

}

 

int CMessageCrackerView::OnCreate(LPCREATESTRUCT lpcs)

{

     SetTimer(1,1000);

     return 0;

}

 

void CMessageCrackerView::OnDestroy()

{

     KillTimer(1);

}

 

void CMessageCrackerView::OnTimer(UINT_PTR nIDEvent)

{

     SYSTEMTIME st;

 

     switch (nIDEvent) {

     case 1:

          GetLocalTime(&st);

          sTime.Format(TEXT("%d:%d:%d"),st.wHour,st.wMinute,st.wSecond);

          Invalidate();

          break;

     }

}

 

void CMessageCrackerView::OnLButtonDown(UINT nFlags, CPoint point)

{

     CClientDC dc(m_hWnd);

    

     dc.TextOut(point.x,point.y,TEXT("Click"));

}

 

OnTimer 함수는 타이머 메시지를 발생시킨 타이머의 ID를 인수로 전달받으므로 이 값을 바로 읽으면 된다. OnLButtonDown은 눌러진 좌표를 CPoint 객체로 받으므로 lParam을 직접 상하위로 쪼개서 읽지 않아도 된다. 인수가 분리되어 전달되므로 과연 편리하기는 하다. 그렇다면 각 메시지의 핸들러 함수 원형은 어떻게 알 수 있을까? 인수를 어떤식으로 분리하는지 MSDN을 매번 뒤져볼 수는 없는 노릇이다.

WTL은 이에 대해 별다른 해결책을 제공하지는 않지만 atlcrack.h 헤더 파일에 주석으로 핸들러 함수의 원형을 기록해 놓았다. 헤더 파일을 참조하여 원형을 복사해 오면 된다. 예를 들어 WM_LBUTTONDOWN 메시지에 대한 핸들러를 작성하고 싶다면 MSG_WM_LBUTTONDOWN 매크로 선언문을 참조한다. F12를 누르면 매크로 선언부로 즉시 이동한다.

 

// void OnLButtonDown(UINT nFlags, CPoint point)

#define MSG_WM_LBUTTONDOWN(func) \

     if (uMsg == WM_LBUTTONDOWN) \

     { \

          SetMsgHandled(TRUE); \

          func((UINT)wParam, _WTYPES_NS::CPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))); \

          lResult = 0; \

          if(IsMsgHandled()) \

              return TRUE; \

     }

 

매크로 정의문 위에 핸들러 함수의 원형이 주석으로 명시되어 있다. 이 주석을 복사해서 원형 선언하고 본체를 만든다. 특별한 기술이 있는 것은 아니고 개발자가 본업인 Ctrl+C, Ctrl+V질만 몇 번 하면 된다. 직접 타이핑하는 수고보다는 낫지만 어쨌든 수작업이기 때문에 MFC만큼 편리하지는 않다.

메시지 크래커를 쓸 것인가 아니면 개발툴이 지원하는 단순 메시지 맵을 쓸 것인가는 취향에 따라 선택해야 할 문제이되 나 같으면 그냥 개발툴의 지원을 받아 핸들러를 만들고 인수는 내가 적당히 알아서 잘라서 쓰는 쪽을 택할 것 같다. 메시지 크래커 지원까지 바란다면 차라리 MFC를 쓰는 것이 더 어울리지 않을까.

.명령 핸들러

명령이란 구체적으로 WM_COMMAND 메시지를 의미하며 WM_NOTIFY도 명령으로 분류된다. 이 두 메시지는 주로 차일드로부터 전달되는 통지 메시지를 처리하는 역할을 한다. 운영체제의 입장에서 볼 때 다른 메시지와 별반 다를 것이 없지만 차일드의 수가 굉장이 많을 수 있고 통지 코드가 컨트롤별로 세분화되어 있다는 점이 다르다.

이 메시지에 대한 핸들러를 일반 함수 하나로 만들게 되면 거대한 switch문을 작성해야 하며 핸들러의 길이도 지나치게 길어지는 문제점이 있다. 그래서 MFC와 WTL은 이 두 메시지를 다른 메시지와는 조금 다르게 처리한다. 컨트롤별로, 통지 메시지별로 세분화하여 핸들러 함수를 따로 만들 수 있도록 되어 있다.

 

: WTLMenu

WTLMenu라는 이름으로 프로젝트를 만들어 보자. 마법사가 만든 프로젝트에는 기본 메뉴가 이미 포함되어 있는데 껍데기만 만들어져 있을 뿐 MFC 수준의 구현은 제공되지 않는다. File 메뉴의 Open을 선택해도 파일 열기 대화상자가 열리지 않으며 Edit 메뉴의 항목들도 대부분 메뉴만 만들어져 있는 상태이다.

이 메뉴의 구현 코드는 사용자가 직접 작성해 넣어야 한다. 여기서는 아예 새로운 항목을 추가하고 명령 핸들러를 작성해 보자. View 메뉴의 아래쪽에 Message Box 항목을 추가한다. ID는 자동으로 ID_VIEW_MESSAGEBOX라고 붙는데 디폴트가 무난하므로 이대로 사용하면 된다.

메뉴 명령에 대한 핸들러를 작성하는 방법도 메시지 핸들러를 만드는 방법과 동일하되 속성창의 메시지 페이지가 아닌 이벤트 페이지를 사용한다는 점이 다르다. 클래스 뷰에서 CMainFrame을 선택해 놓고 속성창의 이벤트 페이지에서 이 항목에 대한 핸들러를 추가한다. 메뉴는 메인 윈도우에 붙어 있는 것이며 메뉴 항목에 대해 WM_COMMAND를 받는 주체가 메인 윈도우이므로 뷰에 핸들러를 추가해서는 안된다.

명령 핸들러를 작성하면 메시지 맵에 COMMAND_ID_HANDLER 엔트리가 추가된다.

 

     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)

          COMMAND_ID_HANDLER(ID_VIEW_MESSAGEBOX, OnViewMessagebox)

          CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)

          CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)

     END_MSG_MAP()

 

명령을 발생시키는 메뉴의 ID와 이 명령을 처리할 함수의 이름이 엔트리에 작성된다. 대충 보면 어떤 형식인지 감을 잡을 수 있다. 그리고 명령 핸들러의 원형이 선언되고 구현 파일에는 본체가 작성된다. 본체안에 원하는 코드를 작성한다.

 

LRESULT CMainFrame::OnViewMessagebox(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

     MessageBox(TEXT("메시지 박스입니다."),TEXT("메롱"));

 

     return 0;

}

 

명령을 제대로 전달받았음을 확인하기 위해 메시지 박스만 열어 보았다. 물론 실제 프로젝트에서는 명령에 따른 처리를 수행해야 한다.

메뉴는 단순히 선택만 하므로 통지 코드 따위를 점검할 필요가 없지만 차일드 컨트롤의 경우는 통지 메시지에 따라 다른 처리를 하기도 해야 한다. 통지 메시지의 종류는 MSDN을 참조해야 하고 switch문을 구성하여 Win32 식으로 코딩해야 한다. 통지 메시지나 컨트롤 ID별로 핸들러를 할당하는 매크로도 정의되어 있기는 하지만 수작업을 해야 하므로 불편하다.

다음은 키보드 엑셀러레이터를 정의해 보자. 리소스 뷰에서 엑셀러레이터 테이블을 열고 같은 ID로 추가만 하면 된다. 단축키는 Ctrl+M으로 지정한다. WTL 내부에 엑셀러레이터에 대한 처리 코드가 이미 작성되어 있으므로 리소스에 등록만 해 놓으면 엑셀러레이터가 동작한다. 이제 메뉴를 선택하나 엑셀러레이터를 누르나 메시지 박스가 잘 열릴 것이다.

리소스에 메뉴 항목 추가하고 그 핸들러를 만드는 실습을 해 보았는데 MFC와 완전히 같은 방법을 사용하므로 아주 쉽다고 느낄 것이다. 그러나 WTL은 구조적으로 MFC와 다른 부분이 있는데 메뉴에 대한 선택 처리는 반드시 메인 프레임이 해야 한다는 점이다. 메뉴 명령이 뷰와 관련된 명령일지라도 메뉴의 핸들러를 뷰에 작성할 수는 없다.

MFC에 익숙한 사람에게는 당황스러운 제약이라고 느껴질텐데 메뉴는 메인 윈도우에 소속되므로 메인 윈도우가 메뉴를 처리하는 것이 원칙적이며 자연스러운 것이다. Win32에서도 메뉴 선택에 대한 메시지를 차일드가 받는 법은 없으며 차일드로 WM_COMMAND가 날라가지는 않는다. 그러므로 WTL도 당연히 Win32와 마찬가지 제약이 생기는 것이다.

그렇다면 MFC의 경우는 어떨까? MFC 프레임워크에는 이벤트 라우팅이라는 알고리즘이 있어서 뷰, 도큐멘트, 프레임, 응용 프로그램 순으로 메시지를 차례대로 보내도록 되어 있으며 이 중 메시지를 먼저 보는 쪽이 처리하도록 되어 있다. 그래서 누구든지 자신과 관련된 명령을 처리할 수 있는 것이다. 이런 면을 보면 역시 MFC는 고수준 라이브러리이며 확실히 돈받고 팔아먹을만한 제품인 것이다.

WTL은 공짜인데다 작고 가벼움을 표방하는 라이브러리이므로 이런 섬세한 알고리즘이 내장되어 있지 않다. 정 뷰에서 처리하려면 일단 메인 프레임이 받은 후 뷰에게 넘겨 주어야 하는데 핸들러를 이중으로 작성해야 하므로 불편하다. 즉, 이벤트 라우팅이 수동이라는 얘기다. MFC의 이벤트 라우팅이 아쉬운 대목이다.

.UI 상태 관리

응용 프로그램이 제공하는 UI들은 현재 상태에 따라 사용할 수 있는 명령의 종류가 달라지고 선택 상태도 수시로 변한다. WTL은 메뉴, 메뉴 항목, 툴바, 상태란, 차일드 윈도우 등 다섯 가지 UI에 대한 상태를 관리한다. WTL의 UI 상태 관리는 여러 가지 요소들이 개입하여 협동적으로 동작한다. 메임 프레임 선언문을 보면 CUpdateUI 믹스인 클래스로부터 상속을 받는데 이 클래스가 UI 관리를 하는 주체이다.

CUpdateUI 클래스안에 UI의 상태를 변경하는 여러 가지 멤버 함수들이 포함되어 있다. 메뉴나 차일드 컨트롤의 UI를 변경할 때는 이 함수들을 호출하면 된다. 메인 윈도우 상단에 부착되어 있는 툴바는 항상 보이므로 UI를 관리할 특별한 시점이 따로 없다. 그래서 아이들 타임에 틈틈이 상태를 관리하는데 CIdleHandler가 메시지 루프의 적당한 때에 OnIdle 함수를 부르고 이 함수에서 툴바를 관리한다.

 

: UIUpdate

내부 동작은 매크로를 소스를 들여다 보면 어렵지 않게 분석할 수 있다. 간단한 인터넷 영화 상영 프로그램을 만들어 보자. 파일 메뉴 앞에 새 팝업 메뉴를 만들고 그림과 같이 디자인한다.

WTL 마법사가 만드는 리소스는 디폴트로 영어로 설정되어 있으므로 리소스의 언어 설정을 한국어로 바꿔야 한글을 쓸 수 있다. 메뉴 항목의 ID는 각각 ID_CONNECT, ID_MOVIE로 준다. 인터넷 연결 상태를 저장하기 위한 멤버 변수를 메인 프레임에 선언한다.

 

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

          public CMessageFilter, public CIdleHandler

{

public:

     DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)

     bool m_Connect;

 

최초 접속되지 않은 상태로 시작하므로 OnCreate에서 false로 초기화한다.

 

LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     ....

     m_Connect=false;

 

     return 0;

}

 

두 메뉴 항목에 대한 핸들러를 다음과 같이 작성한다. 진짜 접속이나 영화 상영을 하지는 않고 메시지 박스로 흉내만 낸다.

 

LRESULT CMainFrame::OnConnect(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

     m_Connect=!m_Connect;

     if (m_Connect == true) {

          MessageBox(_T("인터넷 연결되었습니다."),_T("알림"));

     } else {

          MessageBox(_T("인터넷 접속이 해제되었습니다."),_T("알림"));

     }

 

     return 0;

}

 

LRESULT CMainFrame::OnMovie(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

     MessageBox(_T("지금부터 영화를 상영하겠습니다."),_T("알림"));

 

     return 0;

}

 

동작은 하지만 뭔가 싱겁다. 연결되었다는 것을 메뉴에 표시하지도 않고 연결도 되지 않은 채로 영화를 볼 수 있으니 말이다. 상태 변화에 따라 메뉴 항목을 적절히 관리해야 한다. 메인 프레임의 헤더 파일에 UI 맵을 작성한다.

 

BEGIN_UPDATE_UI_MAP(CMainFrame)

     UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP)

     UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP)

     UPDATE_ELEMENT(ID_CONNECT, UPDUI_MENUPOPUP)

     UPDATE_ELEMENT(ID_MOVIE, UPDUI_MENUPOPUP)

END_UPDATE_UI_MAP()

 

UI 맵의 엔트리에서 관리 대상 항목의 ID와 종류를 지정한다. 마법사에 의해 이미 두개의 UI 맵이 작성되어 있으며 이 둘은 이미 잘 동작한다. UI맵을 작성한 후 필요할 때마다 CUpdateUI의 멤버 함수를 호출하여 UI의 상태를 관리한다.

 

BOOL UIEnable(int nID, BOOL bEnable, BOOL bForceUpdate = FALSE)

BOOL UISetCheck(int nID, int nCheck, BOOL bForceUpdate = FALSE)

BOOL UISetRadio(int nID, BOOL bRadio, BOOL bForceUpdate = FALSE)

BOOL UISetText(int nID, LPCTSTR lpstrText, BOOL bForceUpdate = FALSE)

BOOL UISetState(int nID, DWORD dwState)

DWORD UIGetState(int nID)

 

원형을 보이는 이상 더 무슨 잔소리가 필요하겠는가. 메인 프레임은 CUpdateUI의 파생 클래스이므로 별다른 준비를 할 필요없이 그냥 함수를 호출하면 된다. 이 예제의 경우 인터넷 연결 시점에서 메뉴 항목을 관리한다.

 

LRESULT CMainFrame::OnConnect(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

     m_Connect=!m_Connect;

     if (m_Connect == true) {

          MessageBox(_T("인터넷 연결되었습니다."),_T("알림"));

     } else {

          MessageBox(_T("인터넷 접속이 해제되었습니다."),_T("알림"));

     }

 

     UISetCheck(ID_CONNECT, m_Connect);

     UIEnable(ID_MOVIE, m_Connect);

 

     return 0;

}

 

연결되면 인터넷 연결 항목에 체크 표시를 붙이고 영화 보기 항목을 선택할 수 있다. 영화 보기는 초기에 사용 금지 상태여야 하므로 OnCreate에서도 관리가 필요하다.

 

LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     ....

     m_Connect=false;

     UIEnable(ID_MOVIE, m_Connect);

 

     return 0;

}

 

이제 실행해 보면 프로그램의 상태에 따라 메뉴 항목이 관리될 것이다. 최초 영화 보기 항목이 사용 금지되어 있으며 연결을 선택하면 체크 표시가 나타나 연결 상태를 보여주고 영화 보기 명령도 사용할 수 있다.

 

전형적인 UI인 메뉴 항목에 대해서만 예제를 만들어 보았는데 툴바나 상태란, 차일드 컨트롤을 관리하는 방법도 유사하다.