Custom-Drawn Win32 Tooltips

Custom-Drawn Win32 Tooltips

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