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

Advertisements

Converting C++ Member Functions into Function Objects

Let’s say you have a C++ function that takes a function object as a parameter and calls it:

template <typename _Fn>
void call_functor(_Fn fn)
{
    fn();
}

Now let’s say you want to pass a class’s member function to call_functor() above, as in:

class C
{
    void foo() { std::cout << "foo()\n"; }
};
     
C c;
call_functor(/* What do I put here? c.foo and &C::foo don’t work */);

The STL has a pointer-to-member function adapter called std::mem_fun() which almost gets us there. Unfortunately, it doesn’t quite meet our needs because it requires us to pass a pointer to an instance of C, as in:

C c;
std::mem_fun(&C::foo)(&c); // The &c is the problem

However, we can use std::mem_fun() if we could figure out a way to create a new functor around std::mem_fun() with &c bound as its first parameter. Unfortunately, we cannot use the STL binders (std::bind1st and std::bind2nd) because they only work on binary functions, not unary functions.

In the general case, you should use Boost’s very powerful binding library bind. However, let’s write our own simple binder for expository purposes.

First, we need a function, bind(), that returns a function object which binds a parameter, p1, to a unary function object, func. We’ll call the returned function object binder.

template <typename _Func, typename _P1>
inline binder<_Func, _P1> bind(_Func func, _P1 p1)
{
    return binder<_Func, _P1>(func, p1);
}

The class binder should store func and p1 and have an operator() which calls func with p1 as its parameter. For simplicity we’ll assume func returns void:

template <typename _Func, typename _P1>
class binder
{
public:
    binder(_Func func, _P1 p1) :
        func_(func), p1_(p1) {}
    void operator()() const { return func_(p1_); }
     
private:
    _Func func_; // The functor to apply
    _P1 p1_; // The first paramter
};

We can now solve the initial problem by combining our bind() with std::mem_fun():

call_functor(bind(std::mem_fun(&C::foo), &c));

We can make usage a little more convenient by introducing a macro:

#define mem_fun_functor(c, memFn) bind(std::mem_fun(memFn), &c)
     
call_functor(mem_fun_functor(c, &C::foo));

There’s plenty of room for improvements, but it’s amazing what you can do with a little template trickery.