최상단 광고

2012년 4월 11일 수요일

툴바의 스타일<게임프로그래밍>


. 툴바의 스타일

툴바의 스타일은 CreateToolBarEx 함수의 두번째 인수로 지정한다. WS_CHILD는 선택의 여지 없이 주는 것이며 이 외에 WS_VISIBLE, WS_BORDER 등의 표준 윈도우 스타일을 추가로 지정할 수 있다. 표준 스타일외에 툴바의 고유한 스타일도 물론 이 인수로 지정하는데 공통 컨트롤 버전에 따라 지원되는 스타일의 종류가 약간씩 달라진다. 툴바의 스타일은 다음과 같다.

스타일
설명
TBSTYLE_TOOLTIPS
툴바와 함께 툴팁을 만들도록 한다. 툴팁은 마우스 커서가 툴 버튼 위에 일정시간동안 머무를 경우 나타나는 조그만 윈도우이며 버튼의 기능을 간략하게 설명해 준다. 이 스타일을 준 후 TTN_GETDISPINFO 통지 메시지를 처리해 주어야 한다.
TBSTYLE_WRAPABLE
툴 버튼은 보통 한 줄로 배치하지만 이 스타일을 주면 여러 줄로 버튼을 배치할 수 있도록 한다.
TBSTYLE_ALTDRAG
Alt키와 함께 버튼을 드래그하여 위치를 바꿀 수 있도록 한다. 버튼 드래그를 위해서는 CCS_ADJUSTABLE 스타일도 주어야 한다.
TBSTYLE_FLAT
평평한 툴바를 만든다. 평평한 모양의 툴바는 투명하며 핫 트래킹이 가능해진다. 버튼의 텍스트는 비트맵 아래 나타난다.
TBSTYLE_LIST
평평한 툴바의 텍스트를 비트맵 아래가 아닌 오른쪽에 나타나도록 한다.
TBSTYLE_CUSTOMERASE
배경을 지울 때 NM_CUSTOMDRAW 통지 메시지를 보내도록 한다.
TBSTYLE_TRANSPARENT
투명한 툴바를 만든다. 그러나 툴바는 투명해지지만 버튼은 투명해지지 않는다.
TBSTYLE_REGISTERDROP
커서가 버튼 위로 지나갈때 TBN_GETOBJECT 통지 메시지를 보내며 이 통지 메시지에서 드롭 타겟을 구한다.

ToolBtnText 예제로 이 스타일들을 테스트해 보도록 하자. 이 예제에는 이미 TBSTYLE_FLAT , TBSTYLE_TOOLTIPS 두 개의 툴바 스타일이 적용되어 있어서 평평한 모양의 툴바가 만들어지며 각 버튼은 툴팁을 보여준다. 다음은 TBSTYLE_LIST 스타일을 추가로 준 것인데 문자열이 버튼 아래쪽에 나타나지 않고 오른쪽에 나타난다.

TBSTYLE_TOOPTIPS 스타일을 주면 툴바는 마우스가 버튼 위에서 1초 이상 머무를 때 부모 윈도우에게 TTN_GETDISPINFO(=TTN_NEEDTEXT) 통지 메시지를 보내 툴팁 텍스트를 요구한다. 부모 윈도우는 이 메시지를 받았을 때 어떤 툴 버튼의 툴팁을 요구하는지 조사해 보고 적절한 문자열을 돌려주면 된다.
표준 컨트롤의 통지 메시지는 WM_COMMAND로 전달되지만 공통 컨트롤의 통지 메시지는 WM_NOTIFY메시지 형태로 전달된다. 이 메시지의 wParam은 메시지를 보낸 컨트롤의 ID이며 lParam은 다음과 같이 선언된 구조체이다.

typedef struct tagNMHDR {
    HWND hwndFrom;
    UINT idFrom;
    UINT code;
} NMHDR;

이 구조체는 통지 메시지를 보낸 컨트롤의 윈도우 핸들(hwndFrom)과 ID(idFrom)를 가지며 또한 어떤 종류의 통지 메시지인가를 알려주는 code라는 멤버를 가지고 있다. code가 TTN_GETDISPINFO일 경우 lParam으로는 다음과 같이 선언된 구조체의 포인터가 전달된다.

typedef struct tagNMTTDISPINFO {
    NMHDR      hdr;
    LPTSTR     lpszText;
    char       szText[80];
    HINSTANCE  hinst;
    UINT       uFlags;
#if (_WIN32_IE >= 0x0300)
    LPARAM     lParam;
#endif
} NMTTDISPINFO, FAR *LPNMTTDISPINFO;

부모 윈도우는 TTN_GETDISPINFO 통지 메시지를 받았을 때 다음 세 가지 방법중의 하나로 툴팁에 사용할 문자열을 돌려주어야 한다.

 szText에 문자열을 복사해 준다. 이 경우 80자를 넘어서는 안된다.
 lpszText에 문자열의 번지를 대입해 준다.
 lpszText에 문자열 리소스의 ID를 대입하고 hInst에 리소스를 가진 인스턴스 핸들을 대입해 준다.

이 예제에서는 두 번째 방법을 사용하여 툴팁으로 사용할 문자열을 지정하였다. 코드를 다시 한 번 더 보자.

   case WM_NOTIFY:
      switch (((LPNMHDR)lParam)->code) {
      case TTN_GETDISPINFO:
          switch (wParam) {
          case 10:
             ((LPNMTTDISPINFO)lParam)->lpszText="장남 버튼입니다.";
             break;
          case 11:
             ((LPNMTTDISPINFO)lParam)->lpszText="귀염둥이 둘째 딸입니다.";
             break;
          case 12:
          case 13:
             ((LPNMTTDISPINFO)lParam)->lpszText="쌍둥이 막내들입니다.";
             break;
          }
      }
      return 0;

먼저 lParam으로 전달된 NMHDR 구조체의 code가 TTN_GETDISPINFO인지 점검한다. 그리고 wParam으로 어떤 툴 버튼이 이 메시지를 보냈는가 검사한 후 적절한 텍스트를 lParam으로 전달된 NMTTDISPINFO구조체의 lpszText 멤버에 대입해 주었다. 실행해 보면 툴팁이 제대로 출력될 것이다.

과연 그렇기는 하다. 그런데 위 코드를 보면 정말 이상하다. lParam으로 NMHDR도 전달되고 NMTTDISPINFO라는 구조체도 전달되고 있다. 어떻게 하나의 인수에 두 구조체가 전달될 수 있다는 말인가? 또 이 메시지는 도대체 WM_NOTIFY 메시지인가 TTN_GETDISPINFO 메시지인가? 이때 전달되는 wParam 메시지는 정확하게 어떤 의미를 가지고 있는가?
무척 복잡하다. 이 메시지가 이렇게 복잡한 구조를 가지게 된 데는 역사적인 이유가 있는데 WM_NOTIFY 메시지가 표준 컨트롤을 제외한 모든 컨트롤에 공통적으로 사용되는 중요한 메시지인만큼 그 이유에 대해 잘 알아둘 필요가 있다. 설사 MFC같은 고급 툴을 사용하더라도 이 메시지에 대해서는 정확하게 이해를 하고 있어야 한다.
표준 컨트롤은 자신에게 어떤 변화가 생겼을 때 WM_COMMAND 메시지로 통지 메시지를 전달한다. 이때 변화의 종류가 무엇인지는 HIWORD(wParam)으로 전달되는데 이를 통지 코드라고 한다. lParam은 메시지를 보낸 윈도우 핸들, LOWORD(wParam)은 컨트롤의 ID가 전달된다. 표준 컨트롤은 간단하기 때문에 WM_COMMAND 메시지로 모든 통지 메시지를 전달하는데 아무 문제가 없었다. 통지 메시지가 단순히 사건의 발생을 알리는 이상의 기능이 없었기 때문이다.
그런데 윈95에 와서 컨트롤들이 점점 복잡해지게 되자 통지 메시지로 복잡한 정보를 전달해야 할 필요가 생겼고 게다가 길이가 긴 리턴값까지 요구하는 경우도 생겼다. 그런데 통지 메시지용으로 사용하던 WM_COMMAND는 이미 64비트의 정보에 모두 의미를 할당해버렸기 때문에 더 이상 추가 정보를 수용할 공간이 없다. 그래서 통지 메시지 전용의 WM_NOTIFY라는 메시지를 새로 만들었으며 wParam, lParam을 직접 쓰지 않고 NMHDR라는 구조체의 포인터를 전달하도록 했다. NMHDR 구조체에는 윈도우 핸들, ID, 통지코드 세 가지 정보가 들어간다.
그런데 이렇게 만들어도 정보 전달 공간은 여전히 부족하다. 그래서 lParam에 더 큰 구조체의 포인터를 전달하기로 하되 이 구조체의 첫 번째 멤버는 반드시 NMHDR 구조체로 선언하도록 하였다. 앞에서 보인 NMTTDISPINFO구조체를 보자.

첫 번째 멤버가 NMHDR 구조체로 선언되어 있다. 그래서 lParam은 NMTTDISPINFO구조체의 포인터를 전달함과 동시에 NMHDR 구조체의 포인터를 전달한다고 할 수 있다. 어차피 lParam으로 전달되는 값은 구조체 자체가 아니고 구조체의 포인터이기 때문에 lParam을 어떻게 캐스팅하느냐에 따라 lParam은 NMHDR도 될 수 있고 NMTTDISPINFO가 될 수도 있다. 그래서 다음 코드는 문법적으로 전혀 문제 없다.

   case WM_NOTIFY:
      switch (((LPNMHDR)lParam)->code) {
      case TTN_GETDISPINFO:
          switch (wParam) {
          case 10:
             ((LPNMTTDISPINFO)lParam)->lpszText="장남 버튼입니다.";

WM_NOTIFY 메시지를 받았을 때 우선 통지 코드에 따른 분기를 위해 ((LPNMHDR)lParam)->code값을 읽었다. LPNMHDR로 lParam을 캐스팅했으므로 이때 lParam이 가리키는 번지는 NMHDR 구조체가 된다. 같은 원리로 NMTTDISPINFO구조체의 멤버가 필요할 때는 (LPNMTTDISPINFO)lParam)->lpszText와 같이 LPNMTTDISPINFO로 lParam을 캐스팅하면 된다. 어떻게 캐스팅을 하더라도 lParam이 가리키는 번지는 동일하지만 참조할 수 있는 멤버의 종류가 달라지는 것이다.
한편으로 생각하면 무척 복잡하고 사실 지저분하기도 하지만 C의 캐스트 연산을 제대로 사용하는 재미있는 코드라고도 할 수 있다. WM_NOTIFY 메시지를 정확하게 이해하는 것은 앞으로 공부하게 될 공통 컨트롤을 이해하는데 아주 중요하다. 저런 캐스팅이 어째서 가능한지 아리송하다면 이해될 때까지 생각해 보거나 옆 사람을 괴롭혀서라도 반드시 알고 넘어가도록 하자. 리스트 뷰나 트리 뷰 같은 컨트롤은 그 자체만으로도 엄청나게 많은 기능을 제공하는데 이런 컨트롤을 공부할 때 WM_NOTIFY가 왜 저렇게 되지? 하는 의문이 남아 있다면 그 좋은 기능을 제대로 습득할 수 없을 것이다.
요약해서 정리하자면 공통 컨트롤은 통지 메시지를 보낼 때 WM_NOTIFY 메시지를 사용하며 lParam으로 통지 메시지 고유의 구조체를 전달한다. 이 구조체의 첫번째 멤버는 NMHDR이며 캐스팅하기에 따라 NMHDR의 멤버를 읽을 수도 있고 고유의 구조체 멤버를 읽을 수도 있다.
툴 바는 앞서 설명한 스타일들 외에도 세가지 확장 스타일을 별도로 가진다. 확장 스타일은 TB_SETEXTENDEDSTYLE이라는 별도의 메시지를 보내 설정하는데 lParam으로 확장 스타일 플래그의 조합을 지정한다. 확장 스타일을 조사할 때는 TB_GETEXTENDEDSTYLE 메시지를 보낸다.

확장 스타일
설명
TBSTYLE_EX_
DRAWDDARROWS
드롭 다운 스타일의 버튼 오른쪽에 아래쪽 화살표 이미지를 따로 보여준다. 드롭 다운 버튼은 WM_COMMAND 메시지를 보내며 아래쪽 화살표 버튼이 통지 메시지를 대신 보낸다.
TBSTYLE_EX_
HIDECLIPPEDBUTTONS
일부가 가려진 버튼은 아예 숨기도록 한다. 주로 리바와 함께 사용될 때 이 스타일이 사용되며 인접 밴드가 버튼의 일부를 가렸을 때 버튼을 숨기도록 한다.
TBSTYLE_
EX_MIXEDBUTTONS
TBSTYLE_LIST 스타일과 함께 사용되며 모든 버튼이 텍스트를 가질 수 있도록 하되 TBSTYLE_SHOWTEXT 스타일을 가진 버튼에 대해서만 텍스트를 보여주도록 한다.

두번째 확장 스타일은 설명만으로도 쉽게 이해가 되겠지만 나머지 두 확장 스타일은 툴바 자체에 적용되는 것이 아니라 툴 버튼의 스타일과 함께 동작하기 때문에 설명만 읽어서는 이해하기 어렵다. 이중 드롭 다운 스타일에 대해서는 잠시 후 예제를 만들어서 툴 버튼의 스타일과 함께 연구해 볼 것이다.

댓글 없음: