Coding in-place tooltips


Today we’ll look at how to implement in-place tooltips. These are tooltips that appear when the user hovers the mouse over a string that cannot be displayed in its entirety. The tooltip overlays the partially-displayed text and provides the remainder of the text that had been truncated. The keys to this technique are the TTN_SHOW notification (which lets you adjust the positioning of a tooltip before it is shown) and the TTM_ADJUSTRECT message which tells you precisely where you need the tooltip to be.

Start with our scratch program and add the following:

HFONT g_hfTT;
HWND g_hwndTT;
RECT g_rcText;
LPCTSTR g_pszText = TEXT("Lorem ipsum dolor sit amet.");
const int c_xText = 50;
const int c_yText = 50;

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
 g_hwndTT = CreateWindowEx(WS_EX_TRANSPARENT, TOOLTIPS_CLASS, NULL,
                           TTS_NOPREFIX,
                           0, 0, 0, 0,
                           hwnd, NULL, g_hinst, NULL);
 if (!g_hwndTT) return FALSE;

 g_hfTT = GetStockFont(ANSI_VAR_FONT);
 SetWindowFont(g_hwndTT, g_hfTT, FALSE);

 HDC hdc = GetDC(hwnd);
 HFONT hfPrev = SelectFont(hdc, g_hfTT);
 SIZE siz;
 GetTextExtentPoint(hdc, g_pszText, lstrlen(g_pszText), &siz);
 SetRect(&g_rcText, c_xText, c_yText,
                    c_xText + siz.cx, c_yText + siz.cy);
 SelectFont(hdc, hfPrev);
 ReleaseDC(hwnd, hdc);

 TOOLINFO ti = { sizeof(ti) };
 ti.uFlags = TTF_TRANSPARENT | TTF_SUBCLASS;
 ti.hwnd = hwnd;
 ti.uId = 0;
 ti.lpszText = const_cast<LPTSTR>(g_pszText);
 ti.rect = g_rcText;
 SendMessage(g_hwndTT, TTM_ADDTOOL, 0, (LPARAM)&ti);

 return TRUE;
}

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
 HFONT hfPrev = SelectFont(pps->hdc, g_hfTT);
 TextOut(pps->hdc, g_rcText.left, g_rcText.top,
         g_pszText, lstrlen(g_pszText));
 SelectFont(pps->hdc, hfPrev);
}

After declaring a few variables, we dig into our computations at window creation. We create the tooltip window, passing ourselves as the owner window. (Passing ourselves as the owner window is important in order to get proper Z-order behavior. I refer the reader to the fifth of my “Five Things Every Win32 Developer Should Know” topics for further details.) We then obtain our font and set it into the tooltip control so that the tooltip renders in the same font we do. (I’ll take up more complex font manipulation in a future entry.) We then measure our text in the target font and set the g_rcText rectangle to the dimensions of that text. We use that rectangle to establish the boundaries of a tool in the tooltip control. By setting the TTF_SUBCLASS flag, we indicate that the tooltip control should subclass our window in order to intercept mouse messages. This is a convenience to avoid us having to use the TTM_RELAYEVENT message to forward the mouse messages manually. This hooks up the tooltip.

Painting the content is a simple matter of selecting the font and drawing the text.

Run this program and hover over the text. The tooltip appears, but it’s in the wrong place. Aside from that, though, things are working as expected. The tooltip has the correct font, it fires only when the mouse is over the text itself, and it dismisses when the mouse leaves the text. Let’s position the tooltip:

LRESULT
OnTooltipShow(HWND hwnd, NMHDR *pnm)
{
 RECT rc = g_rcText;
 MapWindowRect(hwnd, NULL, &rc);
 SendMessage(pnm->hwndFrom, TTM_ADJUSTRECT, TRUE, (LPARAM)&rc);
 SetWindowPos(pnm->hwndFrom, 0, rc.left, rc.top, 0, 0,
   SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER);
 return TRUE; // suppress default positioning
}

LRESULT
OnNotify(HWND hwnd, int idFrom, NMHDR *pnm)
{
 if (pnm->hwndFrom == g_hwndTT) {
  switch (pnm->code) {
  case TTN_SHOW:
   return OnTooltipShow(hwnd, pnm);
  }
 }
 return 0;
}

// Add to WndProc
    HANDLE_MSG(hwnd, WM_NOTIFY, OnNotify);

The TTN_SHOW notification is sent when the tooltip is about to be displayed. We respond to the notification by mapping the text rectangle to screen coordinates and using the TTM_ADJUSTRECT message to expand the rectangle to include all the margins and borders that the tooltip control will place around the text. That way, when we position the tooltip at that location, the margins and borders match up precisely, and the text appears at the desired location. It is important to return TRUE to indicate to the tooltip control that we took care of positioning the window and it should not do its default positioning.

When you run this program, you will find one more problem: Tooltip animations are still taking place, which is particularly distracting if the animation is a slide animation. This is easy to fix: Tweak the way we create the tooltip control.

 g_hwndTT = CreateWindowEx(WS_EX_TRANSPARENT, TOOLTIPS_CLASS, NULL,
                           TTS_NOPREFIX | TTS_NOANIMATE,
                           0, 0, 0, 0,
                           hwnd, NULL, g_hinst, NULL);

The TTS_NOANIMATE style suppress animations, which means that the tooltip simply pops into place, exactly what we want.

So there you have it—the basics of in-place tooltips. Of course, there are many details you may wish to deal with, such as showing the tooltip only if the string is clipped. But those issues are independent of in-place tooltips, so I won’t go into them here. We’ll look at selected aspects of tooltips in future installments.

Comments (17)
  1. Martin Filteau says:

    The tooltip doesn’t have a black border on Windows 2000.  Any clue ?

    Worst on Vista, the tooltip has a black border once out of four…

  2. Gregory says:

    Why not in C++ style? Did you forget about your new scratch program? :)

    [None of the samples yet are so complicated that they need the C++ version of the scratch program. -Raymond]
  3. macbirdie says:

    As a note regarding GetTextExtentPoint at MSDN says:

    "This function is provided only for compatibility with 16-bit versions of Windows. Applications should call the GetTextExtentPoint32 function, which provides more accurate results."

    So why not use the "proper" one? [besides not needing the increased accurancy]

    Do you suggest to still be compatible with 16-bit Windows or did you use it just for simplicity’s sake? Just curious of your point of view.

    Anyway thanks for the tip, Raymond!

  4. Anthony Wieser says:

    So, how did they get it so wrong in Microsoft Document Explorer for the tree view in the left pane, bundled with VS 2005?

    If the pane is narrow (or the text is wide) the entire pane flickers when the tooltip is displayed.

    Or are you still building with VC 6 Raymond, so haven’t had the japanese style flashing animations?

    [I don’t see why you would reasonably expect me to explain why VS 2005 behaves any particular way. -Raymond]
  5. Norman Diamond says:

    Of course, there are many details you may

    > wish to deal with, such as showing the

    > tooltip only if the string is clipped.

    If your subsequent articles include an example for “if and only if”
    instead of just “only if”, please consider sending it to your
    colleagues who work on Outlook Express.

    (I’m not asking you to explain OE to readers, but please consider
    explaining Windows to your colleagues.  They successfully avoid
    displaying unnecessary tips but they miss some necessary ones.)

    [I have neither the time nor inclination to talk to people about everything you suggest I talk to them about. -Raymond]
  6. Anthony Wieser says:

    Raymond,

    I didn’t expect you to know.  It was more of a rhetorical discussion point; just wondered if you had noticed, and had perhaps applied your "psychic powers" of debugging as you’ve mentioned before.

    Sorry if it came over the wrong way.

    Your timing is spookily weird though sometimes.  Just the other day I built a DDE server for a client, and what pops up, but an article on DDE from you.

  7. Serge Wautier says:

    Raymond,

    This kind of posts are worth gold. Straight to the point, solving real-world programming problems.

    The thing I just can’t understand is how many hours you can pack in 24! Keep up with the great blog.

  8. Changing the font is the most common customization.

  9. Norman Diamond says:

    [I have neither the time

    Understood.  In order to fix all the bugs in Windows, we’d need crackpot science:

    First, imagine we could fix a bug in Windows in one day.

    On the second day, imagine we fix two bugs in Windows.  On the
    third day, four bugs.  Each day, imagine we double the number of
    bugs we fix.

    By the end of the twenty-seventh day, we would imagine that we’ve fixed all of the bugs in Windows.

    I understand.  In the real world, it isn’t going to happen.

    > nor inclination

    Therefore we fix zero.

    For this I almost have less understanding, but actually enough
    experience working for other companies that prohibit bugfixing does
    make me understand.  After a while, everyone who has the
    capability to fix bugs loses interest in trying to buck the system.
     You even mentioned how you were penalized for your efforts to
    help improve bug reduction in Windows 98.  OK, I understand.

    Time for a brainstorm.

    Suppose you imagine that Outlook Express isn’t a crucial integral
    component of a crucial integral component of Windows.  Then you
    could put an appcompat hack into Windows to work around the bug in the
    third-party app.  Oops sorry, this is more crackpot science,
    requiring imagination.  There’s no solution.

    OK, I give up.  Sorry for disturbing you.

    [I’m impressed with your conclusion that because I don’t have the time and inclination to fix one bug in a component owned by a team I am not a member of means that I’m not fixing any bugs at all. -Raymond]
  10. Norman Diamond says:

    I’m impressed with your conclusion

    Be as impressed as you wish.  Customers are located even farther away from that crucial component of a crucial component of your company’s product than you are.  Somehow I have a feeling that even if a customer paid Microsoft’s fee to report a bug, the customer wouldn’t be able to get it fixed either.

    While reading your article, I noticed its relevance to a long-standing bug.  Temporarily I forgot that your company discourages bug fixing internally as much as it does with customers, therefore I made a worthless suggestion.  Again I apologize to you for this lapse.

  11. Vince P says:

    I love how Norman uses your blog as his personal bitch-at-Microsoft forum

  12. Nuno Campos says:

    I’ve implemented something very similar to this some time ago. I
    respond to TTN_SHOW just like you do, but instead of using
    SendMessage(…, TTM_ADJUSTRECT, …), I call SetWindowPos() without
    SWP_NOSIZE and with the desired size. It works fine, but is it wrong?

    [Without TTM_ADJUSTRECT, how do you know how big the tooltip should be (relative to your desired size) and how it should be positioned? Do you just hard-code a number? -Raymond]
  13. J says:

    "therefore I made a worthless suggestion."

    Maybe it’s not that your suggestions are worthless, but instead it’s just that you complain so much that everything you say starts to become this annoying buzz that people try to ignore.  Perhaps you can try to be constructive once in your life and you’ll actually get people who want work with you instead of wanting nothing to do with you.

    Nobody likes a chronic complainer.

  14. Nuno Campos says:

    Sorry, I’ve never heard of TTM_ADJUSTRECT and I should have read the documentation before bothering you with this… You are right, I am “hard-coding” a number: it’s an owner-drawn list with an owner-drawn tooltip, so I know exactly how big the tooltip should be. Before reading about TTM_ADJUSTRECT on MSDN, I thought it was being used only to set the tooltip’s size.

    [Owner-draw gives you control over the insides of the tooltip, but not the outsides (the border, the margin, etc.) -Raymond]
  15. Norman Diamond says:

    Wednesday, June 28, 2006 1:43 PM by J

    > Perhaps you can try to be constructive once

    > in your life

    Well obviously I wasn’t constructive with this one:

    >> If your subsequent articles include an

    >> example for "if and only if" instead of just

    >> "only if", please consider sending it to

    >> your colleagues who work on Outlook Express.

    so I think I’m genetically incapable of it, sorry.  The only constructive things I get to do are code.

    > Nobody likes a chronic complainer.

    Right.  Even I don’t.  I wish that the number of causes for complaint would go down.

Comments are closed.