Like many common controls, the tooltip control supports custom drawing for maximum flexibility. This is a quick tutorial on how to use the tooltip custom draw facility.
First, start with the following scratch program (which is a slightly modified version of Raymond Chen’s scratch program):
-
#define STRICT
-
#include <windows.h>
-
#include <windowsx.h>
-
#include <commctrl.h>
-
#include <tchar.h>
-
-
#define WND_CLASS_NAME TEXT("Scratch")
-
-
HINSTANCE g_hinst;
-
-
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
-
{
-
return TRUE;
-
}
-
-
void OnDestroy(HWND hwnd)
-
{
-
PostQuitMessage(0);
-
}
-
-
LRESULT CALLBACK WndProc(HWND hwnd, UINT uiMsg, WPARAM wParam,
-
LPARAM lParam)
-
{
-
switch (uiMsg)
-
{
-
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
-
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
-
}
-
-
return DefWindowProc(hwnd, uiMsg, wParam, lParam);
-
}
-
-
BOOL RegisterWindowClass()
-
{
-
WNDCLASS wc;
-
ATOM atom;
-
-
wc.style = 0;
-
wc.lpfnWndProc = WndProc;
-
wc.cbClsExtra = 0;
-
wc.cbWndExtra = 0;
-
wc.hInstance = g_hinst;
-
wc.hIcon = NULL;
-
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
-
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
-
wc.lpszMenuName = NULL;
-
wc.lpszClassName = WND_CLASS_NAME;
-
-
atom = RegisterClass(&wc);
-
return (atom != 0);
-
}
-
-
int WINAPI _tWinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
-
LPTSTR lpCmdLine, int nCmdShow)
-
{
-
INITCOMMONCONTROLSEX icc;
-
int ret = EXIT_FAILURE;
-
-
g_hinst = hinst;
-
-
// We will need the tooltip common control
-
icc.dwSize = sizeof(icc);
-
icc.dwICC = ICC_WIN95_CLASSES;
-
if (InitCommonControlsEx(&icc))
-
{
-
if (RegisterWindowClass())
-
{
-
HWND hwnd = CreateWindow
-
(
-
WND_CLASS_NAME,
-
TEXT("Scratch"),
-
WS_OVERLAPPEDWINDOW,
-
CW_USEDEFAULT, CW_USEDEFAULT,
-
CW_USEDEFAULT, CW_USEDEFAULT,
-
NULL,
-
NULL,
-
hinst,
-
0
-
);
-
if (hwnd != NULL)
-
{
-
MSG msg;
-
-
(void) ShowWindow(hwnd, nCmdShow);
-
while (GetMessage(&msg, NULL, 0, 0)) {
-
TranslateMessage(&msg);
-
DispatchMessage(&msg);
-
}
-
-
ret = EXIT_SUCCESS;
-
}
-
}
-
}
-
-
return ret;
-
}
Next, we’ll add a tooltip to this window. We’re not going to do anything fancy like tooltip multiplexing so we’ll use TTF_SUBCLASS.
-
HWND g_hwndTT;
-
-
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
-
{
-
BOOL ret = FALSE;
-
-
// Create the tooltip window
-
g_hwndTT = CreateWindow
-
(
-
TOOLTIPS_CLASS,
-
NULL,
-
TTS_NOPREFIX,
-
CW_USEDEFAULT, CW_USEDEFAULT,
-
CW_USEDEFAULT, CW_USEDEFAULT,
-
hwnd,
-
NULL,
-
g_hinst,
-
NULL
-
);
-
if (g_hwndTT != NULL)
-
{
-
// Tell the tooltip to register itself using the entire scratch
-
// window’s client area as the active region.
-
TOOLINFO ti = { sizeof(ti) };
-
ti.uFlags = TTF_SUBCLASS;
-
ti.hwnd = hwnd;
-
ti.hinst = g_hinst;
-
ti.uId = 0;
-
ti.lpszText = TEXT("Hello world!");
-
if (GetClientRect(hwnd, &ti.rect))
-
{
-
if (SendMessage(g_hwndTT, TTM_ADDTOOL, 0, (LPARAM)&ti))
-
{
-
ret = TRUE;
-
}
-
}
-
}
-
-
// If there were any failures, clean up the allocated objects
-
if (!ret)
-
{
-
if (g_hwndTT)
-
DestroyWindow(g_hwndTT);
-
}
-
-
return ret;
-
}
If you compile and run the program, you should see a tooltip with the text “Hello World!” pop up.
To use custom draw, we must handle the NM_CUSTOMDRAW message. First we’ll write the WM_NOTIFY handler to forward the message to our function.
-
LRESULT OnToolTipCustomDraw(NMTTCUSTOMDRAW* pcd)
-
{
-
// Perform the default action (i.e. have the tooltip control draw
-
// "Hello World!" itself)
-
return CDRF_DODEFAULT;
-
}
-
-
LRESULT OnNotify(HWND hwnd, int idFrom, NMHDR* pnm)
-
{
-
if (pnm->hwndFrom == g_hwndTT)
-
{
-
switch (pnm->code)
-
{
-
case NM_CUSTOMDRAW:
-
return OnToolTipCustomDraw((NMTTCUSTOMDRAW*) pnm);
-
}
-
}
-
-
return 0;
-
}
-
-
LRESULT CALLBACK WndProc(HWND hwnd, UINT uiMsg, WPARAM wParam,
-
LPARAM lParam)
-
{
-
switch (uiMsg)
-
{
-
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
-
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
-
HANDLE_MSG(hwnd, WM_NOTIFY, OnNotify);
-
}
-
-
return DefWindowProc(hwnd, uiMsg, wParam, lParam);
-
}
Now we’ll implement the custom draw function. For simplicity’s sake, we will simply write the text “Hello World!” just as the tooltip did before we used custom draw.
It is important to note that the tooltip control will continue to draw the static text even if we are using custom draw. To get around this problem, we’ll have the tooltip control draw the text in the same color as the background, effectively making it invisible.
-
// Draw the contents of the tooltip within the rectangle prc
-
void DrawToolTipContent(HDC hdc, RECT* prc)
-
{
-
SetTextColor(hdc, RGB(0, 0, 0));
-
TextOut(hdc, prc->left, prc->top, TEXT("Hello World!"), 12);
-
}
-
-
LRESULT OnToolTipCustomDraw(NMTTCUSTOMDRAW* pcd)
-
{
-
switch (pcd->nmcd.dwDrawStage)
-
{
-
case CDDS_PREPAINT:
-
{
-
// Set the text and back colors of default text so it
-
// becomes invisible
-
COLORREF clrBg = (COLORREF) SendMessage(g_hwndTT,
-
TTM_GETTIPBKCOLOR,
-
0, 0);
-
SetTextColor(pcd->nmcd.hdc, clrBg);
-
SetBkColor(pcd->nmcd.hdc, clrBg);
-
return CDRF_NOTIFYPOSTPAINT;
-
}
-
case CDDS_POSTPAINT:
-
{
-
DrawToolTipContent(pcd->nmcd.hdc, &pcd->nmcd.rc);
-
return CDRF_SKIPDEFAULT;
-
}
-
}
-
-
return CDRF_DODEFAULT;
-
}
We now can use the full range of GDI functions to render the content of the tooltip, including multiple fonts, lines, ellipses, etc. However, drawing is limited to the size of the tooltip’s client window, and this is determined based on the text passed to the tooltip window in OnCreate(). If we want to control the size of the tooltip, we must handle the TTN_SHOW message:
-
LPCTSTR g_szTooltipMsg = TEXT("Hello World!");
-
-
// Determine the required size of the client area of the tooltip
-
BOOL GetToolTipContentSize(SIZE* psz)
-
{
-
BOOL ret = FALSE;
-
-
HDC hdc = GetDC(g_hwndTT);
-
if (hdc != NULL)
-
{
-
HFONT hfontTT = (HFONT) SendMessage(g_hwndTT, WM_GETFONT, 0, 0);
-
HFONT hfontTTOld = (HFONT) SelectObject(hdc, hfontTT);
-
if (hfontTTOld != NULL)
-
{
-
SIZE szText;
-
if (GetTextExtentPoint32(hdc, g_szTooltipMsg,
-
lstrlen(g_szTooltipMsg), &szText))
-
{
-
psz->cx = szText.cx;
-
psz->cy = szText.cy;
-
ret = TRUE;
-
}
-
-
SelectObject(hdc, hfontTTOld);
-
}
-
-
ReleaseDC(g_hwndTT, hdc);
-
}
-
-
return ret;
-
}
-
-
// Determine the required client rectangle of the tooltip to fit the
-
// text
-
BOOL GetToolTipContentRect(RECT* prc)
-
{
-
BOOL ret = FALSE;
-
-
SIZE sz;
-
if (GetToolTipContentSize(&sz))
-
{
-
if (GetWindowRect(g_hwndTT, prc))
-
{
-
prc->right = prc->left + sz.cx;
-
prc->bottom = prc->top + sz.cy;
-
ret = TRUE;
-
}
-
}
-
-
return ret;
-
}
-
-
// When the tooltip is being shown, size it to fit the content
-
LRESULT OnToolTipShow(NMHDR* pnm)
-
{
-
LRESULT ret = 0;
-
RECT rc;
-
-
if (GetToolTipContentRect(&rc))
-
{
-
// Adjust the rectangle to be the proper size to contain the
-
// content
-
if (SendMessage(g_hwndTT, TTM_ADJUSTRECT, TRUE, (LPARAM) &rc))
-
{
-
// Resize and move the tooltip accordingly
-
if (SetWindowPos(g_hwndTT, NULL, rc.left, rc.top,
-
rc.right - rc.left, rc.bottom - rc.top,
-
SWP_NOZORDER | SWP_NOACTIVATE))
-
{
-
ret = TRUE;
-
}
-
}
-
}
-
-
return ret;
-
}
-
-
LRESULT OnNotify(HWND hwnd, int idFrom, NMHDR* pnm)
-
{
-
if (pnm->hwndFrom == g_hwndTT)
-
{
-
switch (pnm->code)
-
{
-
case TTN_SHOW:
-
return OnToolTipShow(pnm);
-
case NM_CUSTOMDRAW:
-
return OnToolTipCustomDraw((NMTTCUSTOMDRAW*) pnm);
-
}
-
}
-
-
return 0;
-
}
-
-
void DrawToolTipContent(HDC hdc, RECT* prc)
-
{
-
SetTextColor(hdc, RGB(0, 0, 0));
-
TextOut(hdc, prc->left, prc->top, g_szTooltipMsg,
-
lstrlen(g_szTooltipMsg));
-
}
We now have a sizable, custom-drawable tooltip control.
February 14th, 2008 at 2:42 pm
Hello, very good article. Do u know why it’s not working with balloon tips (flag TTS_BALLOON)? it’s not resized
September 6th, 2008 at 10:59 am
I think I might know the answer to that one. The NM_CUSTOMDRAW handler should not resize the window itself. Instead, it should inspect the uDrawFlags member of the NMTTCUSTOMDRAW structure passed in. If this contains DT_CALCRECT then the tooltip window is asking the handler to calculate the size of the text (or whatever) to be displayed, which the handler can do by updating the rc member of the NMTTCUSTOMDRAW structure.
Sneaky, eh? I’m not sure if it works 100% though as the tooltip window seems to carry out some additional calculations of its own so a bit of experimentation might be in order.
Paul Sanders
http://www.alpinesoft.co.uk
September 7th, 2008 at 5:20 am
Hello again,
Further to my previous post … I have found that you can reduce the size of the tooltip window by the method I described, but you cannot increase it (!!). So Steven is right after all - you have to resize the window yourself at the critical moment. Sorry about that.
There’s a nicer way to stop the tooltip from drawing its own text though - do all your painting in CDDS_PREPAINT and return CDRF_SKIPDEFAULT. The tooltip then gracefully steps aside (which is about the only thing it does do gracefully). This opens the door to fancy backgrounds, for example.