.제자리 툴팁

제자리 툴팁(In place ToolTip) 일반 툴팁과는 용도가 다르다. 일반 툴팁은 툴에 대한 숨겨진 정보를 보여 주지만 제자리 툴팁은 주로 잘려서 보이지 않는, 가려져 있는 나머지 부분을 확인시켜 주기 위해 사용한다. 주로 트리뷰나 리스트뷰같이 표현하는 정보가 많은 컨트롤들이 제자리 툴팁을 채용하고 있는데 탐색기나 MSDN에서 쉽게 확인할 있다.

 

Documents and Settings라는 폴더 이름이 너무 길어 오른쪽이 잘려 보이지 않는데 폴더 위로 커서를 옮기면 폴더명의 나머지 부분을 툴팁으로 확인할 있다. 윈도우를 드래그하여 크기를 늘릴 필요가 없으므로 훨씬 편리하다. MSDN 마찬가지로 토픽명을 이런 식으로 제자리 툴팁을 사용하여 보여 준다. 커스텀 컨트롤이나 일반 윈도우를 작성할 때도 기법을 활용하면 좁은 화면에 많은 정보를 표현하면서도 불편하지 않게 만들 있다.

제자리 툴팁은 여러 가지 방법으로 구현할 있지만 트래킹 툴팁을 사용하는 것이 가장 편리하다. 왜냐하면 트래킹 툴팁은 정확한 좌표에 놓을 있고 툴팁 위에서 커서가 이동해도 툴팁이 사라지지 않기 때문이다. 일반 툴팁은 마우스 커서가 위로 올라오면 즉시 사라지도록 되어 있다. 제자리 툴팁의 텍스트는 선택의 여지 없이 가려지기 전의 전체 텍스트이므로 고민할 필요가 없으며 툴팁 출력 시점과 위치를 결정하는 것이 문제다.

먼저 출력 시점은 텍스트 위에 커서가 있어야 하고 해당 텍스트의 일부가 잘려서 보이지 않는 상태여야 한다. 텍스트 위에 커서가 있더라도 전부 보이는 상황에서는 제자리 툴팁을 보여줄 필요가 없다. 계산을 위해서 텍스트의 출력 위치와 , 높이에 대한 정보를 미리 계산해 놓아야 한다.

출력 위치는 텍스트 바로 위가 되므로 아주 간단할 같지만 약간의 조정이 필요하다. 왜냐하면 툴팁의 윈도우 영역과 텍스트 영역간에는 약간의 오차가 있기 때문이다. 텍스트 좌표에 툴팁 컨트롤을 그냥 놓으면 툴팁 컨트롤 내에서의 경계선과 텍스트와의 여백 차이만큼 글자가 오른쪽 아래로 이동되어 보이므로 별로 좋지 않다. 마치 제자리 툴팁이 텍스트와 같은 위치에서 확장되는 것처럼 보이는 것이 바람직하다.

정확한 위치 계산을 위해서는 툴팁의 텍스트 영역을 윈도우 영역으로 변환해야 하는데 계산은 툴팁의 여백 설정이나 폰트 크기 등에 따라 달라지므로 일반적인 규칙을 찾기는 어렵다. 경험적으로 계산해 보면 2~3픽셀 정도의 차이가 있으므로 값을 바로 적용해도 일단은 괜찮아 보이겠지만 시스템 세팅 변경에 의한 문제 발생의 소지가 있다.

다행히 계산은 툴팁 컨트롤이 대신해 주는데 TTM_ADJUSTRECT 메시지를 보내 주면 된다. wParam으로 변환 방법을 지정하는데 값이 TRUE이면 텍스트 영역을 윈도우 영역으로 변환하며 FALSE이면 반대로 윈도우 영역을 텍스트 영역으로 변환해 준다. lParam에는 변환전의 원본 RECT 주며 변환된 결과가 구조체로 다시 리턴된다. lParam 텍스트 영역을 주고 wParam TRUE 주면 변환된 윈도우 영역이 lParam으로 리턴되는데 좌표에 툴팁 컨트롤을 출력하면 정확한 위치이다. 이론적인 설명은 여기까지이고 이제 예제를 만들어 보고 분석해 보자.

 

#include <commctrl.h>

struct tag_Text

{

int x,y;

SIZE sz;

char *Text;

};

 

tag_Text arText[]={

{200,30,{0,0},"툴팁"},

{200,50,{0,0},"Windows API ToolTip Control"},

{200,70,{0,0},"아주 정말 무척 매우 많이 텍스트입니다."},

{200,90,{0,0}," 문장의 일부가 가려지면 제자리 툴팁이 위에 출력되어 가려진 텍스트를 보여 줍니다."}

};

 

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

{

HDC hdc;

PAINTSTRUCT ps;

INITCOMMONCONTROLSEX iccex;

static HWND hTip;

TOOLINFO ti;

int i;

POINT pt;

RECT crt, trt;

 

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);

 

    // 시스템 글꼴로 변경

    SendMessage(hTip,WM_SETFONT,(WPARAM)(HFONT)GetStockObject(SYSTEM_FONT),0);

 

    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);

 

    // 텍스트의 크기 계산

    hdc=GetDC(hWnd);

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

       GetTextExtentPoint32(hdc,arText[i].Text,lstrlen(arText[i].Text),&arText[i].sz);

    }

    ReleaseDC(hWnd,hdc);

    return 0;

case WM_MOUSEMOVE:

    // 텍스트 영역 안인지 조사

    pt.x=LOWORD(lParam);

    pt.y=HIWORD(lParam);

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

       if (pt.x > arText[i].x && pt.x < arText[i].x+arText[i].sz.cx &&

          pt.y > arText[i].y && pt.y < arText[i].y+arText[i].sz.cy)

          break;

    }

 

    ti.cbSize=sizeof(TOOLINFO);

    ti.uFlags=0;

    ti.hwnd=hWnd;

    ti.uId=0;

 

    // 영역 밖이면 툴팁 숨김

    if (i==sizeof(arText)/sizeof(arText[0])) {

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

       return 0;

    }

 

    // 영역 안이더라도 잘리지 않았으면 상관없다.

    GetClientRect(hWnd,&crt);

    if (crt.right >= arText[i].x+arText[i].sz.cx &&

       crt.bottom >= arText[i].y+arText[i].sz.cy) {

       return 0;

    }

 

    // 텍스트 변경

    ti.lpszText=arText[i].Text;

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

 

    // 위치 조정

    pt.x=arText[i].x;

    pt.y=arText[i].y;

    ClientToScreen(hWnd,&pt);

    SetRect(&trt,pt.x,pt.y,pt.x+arText[i].sz.cx,pt.y+arText[i].sz.cy);

    SendMessage(hTip,TTM_ADJUSTRECT,(WPARAM)TRUE,(LPARAM)&trt);

 

    // 툴팁 보임

    SendMessage(hTip,TTM_TRACKPOSITION,0,(LPARAM)MAKELPARAM(trt.left, trt.top));

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

    return 0;

case WM_PAINT:

    hdc=BeginPaint(hWnd, &ps);

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

       TextOut(hdc,arText[i].x,arText[i].y,arText[i].Text,lstrlen(arText[i].Text));

    }

    EndPaint(hWnd, &ps);

    return 0;

case WM_DESTROY:

    PostQuitMessage(0);

    return 0;

}

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

}

 

길이가 각각 다른 종류의 문자열을 배치했으며 윈도우의 폭을 450으로 지정하여 적당히 텍스트가 잘리도록 했다. 잘려진 텍스트 위로 커서를 이동시켜 보면 제자리 툴팁이 나타날 것이다. 무조건 나타나는 것은 아니면 텍스트가 잘려져 있을 때만 나타난다.

arText 배열은 출력할 문자열과 위치, 그리고 크기값을 가지며 WM_PAINT 메시지에서는 배열의 문자열을 단순히 출력하기만 한다.

WM_CREATE에서 툴팁 컨트롤을 생성하고 시스템 폰트로 변경한다. 제자리 툴팁은 가려진 툴을 대신 보여주는 것이므로 폰트가 동일해야 한다. 물론 툴팁 컨트롤의 폰트로 툴을 출력해도 효과는 동일할 것이다. 트래킹 툴팁을 생성하고 툴의 문자열 크기를 미리 구해 놓는다. 값은 실시간으로 계산해도 되지만 시간이 오래 걸리므로 최초 한번만 미리 계산해 놓는 것이 훨씬 유리하다.

WM_MOUSEMOVE에서는 커서의 위치를 보고 일단 텍스트 영역 안인지를 조사한다. 텍스트 영영 밖에 있으면 툴팁을 숨기고 리턴한다. 영역 안인 경우는 다시 텍스트가 잘려져 있는지 조사하는데 텍스트가 출력된 작업 영역 좌표와 텍스트의 우하단 좌표를 비교해 본다. 폭이나 높이 어느쪽이라도 부족하면 잘린 것이므로 이때는 제자리 툴팁을 출력하고 그렇지 않으면 그냥 리턴한다.

툴의 원래 텍스트로 텍스트를 변경하고 위치를 조정한다. 이때 TTM_ADJUSTRECT 메시지로 툴팁이 출력될 위치를 정확하게 조정하는데 메시지가 어떤 역할을 하는지 확인해 보고 싶다면 메시지를 보내는 코드를 주석 처리한 비교해 보기 바란다. 텍스트와 제자리 툴팁의 위치가 맞지 않아 무척 보기 싫어질 것이다. 텍스트와 위치 조정까지 끝났으면 TTM_TRACKACTIVATE 메시지를 보내 툴팁 컨트롤을 출력한다.