.트래킹 툴팁

앞에서 만든 예제는 텍스트를 실행중에 임의로 변경하였는데 예제의 경우도 툴팁 출력 시점이나 출력 위치는 툴팁 컨트롤이 알아서 결정한다. 프로그램이 텍스트만 변경할 툴팁 출력 시점과 위치에 대해서는 관여를 없었는데 그렇게 수도 있다. 출력 위치나 시점을 프로그램이 결정할 있는 형태의 툴팁을 트래킹 툴팁이라고 하며 TTF_TRACK 플래그를 주면 트래킹 툴팁이 만들어진다.

툴팁 컨트롤은 트래킹 툴팁에 대해서는 언제 어느 위치에 출력할 것인가 전혀 관심을 가지지 않으며 프로그램이 원하는 시점에 원하는 곳에 출력하도록 버려둔다. 따라서 이런 형태의 툴팁은 서브클래싱을 필요도 없고 마우스 메시지를 툴팁 컨트롤로 보내줄 필요도 없다. TTF_TRACK 플래그를 지정했다는 것은 "내가 알아서 할테니 툴팁 너는 가만히 있어"라는 의사 표시이다.

트래킹 툴팁의 보임과 숨김은 TTM_TRACKACTIVATE 메시지로 지정하는데 메시지의 lParam 대상 툴을 지정하고 wParam TRUE(보임) 또는 FALSE(숨김) 지정한다. 툴팁의 출력 위치는 TTM_TRACKPOSITION 메시지로 지정하는데 lParam 하위 워드에 x좌표, 상위 워드에 y좌표를 전달하면 된다. 프로그램이 내부적인 계산에 의해 툴팁을 출력할 때라고 판단되면 메시지로 툴팁의 위치를 지정하고 툴팁을 보이도록 만들면 된다. 그럼 트래킹 툴팁의 전형적인 예제를 만들어 보도록 하자.

 

#include <commctrl.h>

HWND hTip, hBtn;

BOOL gTrackOn=TRUE;

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

HDC hdc;

PAINTSTRUCT ps;

INITCOMMONCONTROLSEX iccex;

TOOLINFO ti;

POINT pt;

char Mes[]="마우스 왼쪽 버튼 = 툴팁 보임/숨김";

 

switch(iMessage) {

case WM_CREATE:

    iccex.dwICC=ICC_WIN95_CLASSES;

    iccex.dwSize=sizeof(INITCOMMONCONTROLSEX);

    InitCommonControlsEx(&iccex);

 

    hTip=CreateWindowEx(WS_EX_TOPMOST,TOOLTIPS_CLASS,NULL,0,

       CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,

       hWnd,NULL,g_hInst,NULL);

 

    ti.cbSize=sizeof(TOOLINFO);

    ti.uFlags=TTF_IDISHWND | TTF_TRACK;// | TTF_ABSOLUTE;

    ti.hwnd=hWnd;

    ti.uId=(WPARAM)hWnd;

    ti.lpszText="마우스를 따라 다니는 툴팁";

    SendMessage(hTip,TTM_ADDTOOL,0,(LPARAM)(LPTOOLINFO)&ti);

    SendMessage(hTip, TTM_TRACKACTIVATE,(WPARAM)TRUE,(LPARAM)&ti);

    return 0;

case WM_MOUSEMOVE:

    if (gTrackOn) {

       GetCursorPos(&pt);

       SendMessage(hTip,TTM_TRACKPOSITION,0,(LPARAM)MAKELPARAM(pt.x,pt.y+25));

    }

    return 0;

case WM_LBUTTONDOWN:

    gTrackOn=!gTrackOn;

    ti.cbSize=sizeof(TOOLINFO);

    ti.uFlags=TTF_IDISHWND;

    ti.hwnd=hWnd;

    ti.uId=(WPARAM)hWnd;

    SendMessage(hTip, TTM_TRACKACTIVATE,(WPARAM)gTrackOn,(LPARAM)&ti);

    return 0;

case WM_PAINT:

    hdc=BeginPaint(hWnd, &ps);

    TextOut(hdc,10,10,Mes,lstrlen(Mes));

    EndPaint(hWnd, &ps);

    return 0;

case WM_DESTROY:

    PostQuitMessage(0);

    return 0;

}

return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

실행해 보면 툴팁이 커서를 따라 가며 움직일 것이다. 툴팁의 보임과 숨김은 마우스 왼쪽 버튼으로 토글한다.

WM_LBUTTONDOWN에서 TTM_TRACKACTIVATE 메시지로 툴팁의 출력 여부를 토글하며 전역 변수 gTrackOn 변수도 같이 토글시킨다. 위치 변경은 WM_MOUSEMOVE 메시지에서 마우스의 위치가 바뀔 때마다 TTM_TRACKPOSITION 메시지를 보내 수행하는데 커서 아래 25픽셀 위치에 툴팁을 출력하였다. , 불필요한 이동 처리를 하지 않기 위해 gTrackOn 전역 변수를 참조하고 있다.

TTF_ABSOLUTE 플래그는 TTF_TRACK 플래그가 있을 때만 사용할 있으며 플래그를 주면 TTM_TRACKPOSITION 메시지로 지정한 위치에 정확하게 출력한다. 플래그를 생략하면 정확한 위치를 사용하지 않고 주변에 툴팁이 위치하게 되며 툴팁에 의해 툴이 가려지지 않도록 준다. 플래그를 빼고 실행해 보면 의미를 금방 이해할 있을 것이다. 툴팁이 결코 윈도우 안으로 들어오지 않는다.

트래킹 툴팁을 TTM_UPDATETIPTEXT 메시지와 함께 사용하면 하나의 툴팁으로 출력 시점, 위치, 텍스트까지 프로그램이 마음대로 프로그래밍할 있다. 툴팁으로 보여줄 정보가 아주 많다면, 예를 들어 100개가 넘는다면 툴들을 일일이 등록하기는 무척 귀찮은 일이고 더구나 스크롤시에 툴의 영역을 재계산하는 것도 만만치 않은 작업이다. 이런 경우는 툴팁 컨트롤에게 툴팁 관리를 맡기지 말고 프로그램이 툴에 대한 정보를 내부적으로 자체 관리하면서 툴팁을 출력하면 오히려 편리하다.

다음 예제는 트래킹 툴팁의 기능을 십분 활용하여 화면상에 출력된 색상에 대한 정보를 툴팁으로 보여준다.

 

#include <commctrl.h>

HWND hTip, hBtn;

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

HDC hdc;

PAINTSTRUCT ps;

INITCOMMONCONTROLSEX iccex;

TOOLINFO ti;

COLORREF Col;

static COLORREF OldCol=0xffffffff;

HBRUSH Brush,OldBrush;

int i;

POINT pt;

 

switch(iMessage) {

case WM_CREATE:

    iccex.dwICC=ICC_WIN95_CLASSES;

    iccex.dwSize=sizeof(INITCOMMONCONTROLSEX);

    InitCommonControlsEx(&iccex);

 

    hTip=CreateWindowEx(WS_EX_TOPMOST,TOOLTIPS_CLASS,NULL,0,

       CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,

       hWnd,NULL,g_hInst,NULL);

 

    ti.cbSize=sizeof(TOOLINFO);

    ti.uFlags=TTF_TRACK | TTF_ABSOLUTE;

    ti.hwnd=hWnd;

   ti.uId=0;

    ti.lpszText="dummy";

    SetRect(&ti.rect,0,0,0,0);

    SendMessage(hTip,TTM_ADDTOOL,0,(LPARAM)(LPTOOLINFO)&ti);

    return 0;

case WM_MOUSEMOVE:

    hdc=GetDC(hWnd);

    Col=GetPixel(hdc,LOWORD(lParam),HIWORD(lParam));

    if (Col != OldCol) {

       OldCol=Col;

       ti.cbSize=sizeof(TOOLINFO);

       ti.uFlags=0;

       ti.hwnd=hWnd;

       ti.uId=0;

       if (Col != RGB(255,255,255) && Col != RGB(0,0,0)) {

          for (i=0;i<sizeof(Tools)/sizeof(Tools[0]);i++) {

             if (Col ==   Tools[i].Col) {

                 ti.lpszText=Tools[i].szTip;

                 SendMessage(hTip,TTM_UPDATETIPTEXT,0,(LPARAM)&ti);

                 pt.x=Tools[i].rt.left+Tools[i].xoff;

                 pt.y=Tools[i].rt.top+Tools[i].yoff;

                 ClientToScreen(hWnd,&pt);

                 SendMessage(hTip,TTM_TRACKPOSITION,0,(LPARAM)MAKELPARAM(pt.x,pt.y));

                SendMessage(hTip, TTM_TRACKACTIVATE,(WPARAM)TRUE,(LPARAM)&ti);

                 break;

             }

          }

       } else {

          SendMessage(hTip, TTM_TRACKACTIVATE,(WPARAM)FALSE,(LPARAM)&ti);

       }

    }

    ReleaseDC(hWnd,hdc);

    return 0;

case WM_PAINT:

    hdc=BeginPaint(hWnd, &ps);

    for (i=0;i<sizeof(Tools)/sizeof(Tools[0]);i++) {

       Brush=CreateSolidBrush(Tools[i].Col);

       OldBrush=(HBRUSH)SelectObject(hdc,Brush);

       if (Tools[i].type == 0) {

          Rectangle(hdc,Tools[i].rt.left,Tools[i].rt.top,

             Tools[i].rt.right,Tools[i].rt.bottom);

       } else {

          Ellipse(hdc,Tools[i].rt.left,Tools[i].rt.top,

             Tools[i].rt.right,Tools[i].rt.bottom);

       }

       SelectObject(hdc,OldBrush);

       DeleteObject(Brush);

    }

    EndPaint(hWnd, &ps);

    return 0;

case WM_ACTIVATEAPP:

    if (wParam == FALSE) {

       ti.cbSize=sizeof(TOOLINFO);

       ti.uFlags=0;

       ti.hwnd=hWnd;

       ti.uId=0;

       SendMessage(hTip, TTM_TRACKACTIVATE,(WPARAM)FALSE,(LPARAM)&ti);

    }

    return 0;

case WM_DESTROY:

    PostQuitMessage(0);

    return 0;

}

return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

화면에 원색 도형을 몇개 그려 놓았으며 도형 위에 커서를 가져가면 색상을 조사해서 툴팁으로 보여준다.

Tools 배열은 툴의 유형과 위치, 텍스트, 툴팁의 위치 등에 대한 정보를 가지는데 프로그램은 정보를 바탕으로 언제, 어느 위치에 툴팁을 출력할 것인가를 결정한다. 구조체 배열이므로 도형을 늘리는 것은 아주 쉬우며 배열 요소만 원하는대로 늘려 주면 된다. 설사 도형이 수백개가 되더라도 프로그램의 논리는 동일하다.

WM_CREATE에서 툴팁을 등록하는데 TTF_TRACK 플래그를 주었으므로 TTF_SUBCLASS 플래그는 주지 않아도 된다. lpszText 어차피 실행중에 변경할 예정이므로 의미가 없고 툴의 출력 여부도 프로그램이 결정하므로 툴의 영역을 지정하는 rect 아무 의미가 없다. 그래서 등록할 대부분의 멤버들은 더미값을 주었다.

툴팁의 출력 시점과 위치는 마우스가 이동할 때인 WM_MOUSEMOVE에서 결정한다. 현재 커서 위치의 색상값을 읽어 일단, 흰색이나 검정색이 아닌지를 보는데 예제의 경우 흰색, 검정색은 툴팁이 출력되지 않아도 되는 영역이다.

흰색, 검정색이 아니면 Tools 배열의 색상과 일치하는 것이 있는지 조사하여 Tools 배열의 인덱스를 결정하고 인덱스로부터 툴팁의 위치, 텍스트를 업데이트한 툴팁을 출력하였다. 흰색이나 검정색이면 출력되어 있는 툴팁을 숨긴다. 코드에서 OldCol 변수는 반드시 필요한 것은 아니나 같은 색상 내에서 움직일 불필요한 처리를 하지 않도록 하기 위한 용도로 사용하고 있다.

WM_PAINT에서는 Tools 배열의 정보에 따라 도형을 출력한다. 예제의 핵심은 바로 Tools 배열인데 배열은 프로그램이 관리하는 툴에 대한 모든 정보를 가진다. 배열 요소는 하나의 완전한 객체를 표현하며 출력 정보, 툴팁 출력 시점과 위치, 기타 운영에 필요한 어떤 정보라도 포함할 있다. 구조체를 어떻게 디자인하고 사용할 것인가는 응용의 영역이므로 프로그램의 목적에 따라 재구성해야 것이다. 지도의 지점에 대한 정보를 가질 수도 있고 설계도면의 무수한 부품에 대한 정보를 가질 수도 있으며 또한 실시간으로 정보의 첨삭및 수정도 가능하다.

예제의 다른 중요한 의미는 사각 영역이 아닌 툴에 대해서도 툴팁을 제공할 있다는 점이다. 툴팁 컨트롤은 윈도우 또는 사각영역에 대해서만 툴팁을 관리할 있지만 트래킹 툴팁은 프로그램의 판단에 따라 툴팁을 출력하므로 툴팁 영역에 대한 제한이 따로 없다. 예제는 색상으로 툴팁 영역을 판별하므로 원이나 세모나 어떤 모양이라도 툴팁 출력 영역으로 지정할 있으며 예제의 노란색 원이 예이다. 만약 다각형에 대한 정보를 가지고 있다면 다각형 영역에 툴팁을 수도 있고 리전을 사용한다면 이론적으로 상상할 있는 모든 모양에 대해 툴팁을 있다.

트래킹 툴팁은 프로그램이 출력 위치와 시점을 통제할 있는 대신 툴팁을 숨기는 작업도 프로그램이 직접 책임을 져야 한다. 툴팁을 숨기는 코드를 제대로 작성하지 않으면 원치않는 시점에 툴팁이 출력되어 있을 수도 있다. 예제도 이런 맹점을 가지고 있는데 파란색 사각형의 왼쪽 끝에 커서를 위치한 재빠르게 마우스를 왼쪽으로 이동해 버리면 툴팁이 사라지지 않는다. 흰색이나 검정색 영역을 거치지 않고 커서가 윈도우 영역을 벗어나 버렸기 때문에 툴팁을 숨길 기회를 가지지 못한 것이다.

문제를 해결하려면 물론 아주 많은 방법들이 있겠지만 예제는 이런 처리까지는 하지 않고 있다. 보기 싫기는 하지만 툴팁이 있다고 해서 문제는 없기 때문이다. 다만 프로그램이 포커스를 잃을 때는 툴팁을 강제로 숨겨 주는 처리는 주고 있는데 WM_ACTIVATEAPP 메시지의 wParam으로 FALSE 전달되었을 , 비활성화될 툴팁을 숨기도록 하였다. 처리를 생략하면 프로그램이 최소화되었을 때도 툴팁이 홀로 출력되어 있는 기이한 사태가 벌어질 있다.