Generating tooltip text dynamically


Our multiplexed tooltip right now is displaying the same string for all items. Let's make it display something a bit more interesting so it's more obvious that what we're doing is actually working.

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
 ...
 // ti.lpszText = TEXT("Placeholder tooltip");
 ti.lpszText = LPSTR_TEXTCALLBACK;
 ...
}

LRESULT
OnNotify(HWND hwnd, int idFrom, NMHDR *pnm)
{
 if (pnm->hwndFrom == g_hwndTT) {
  switch (pnm->code) {
  case TTN_GETDISPINFO:
   {
    NMTTDISPINFO *pdi = (NMTTDISPINFO *)pnm;
    if (g_iItemTip >= 0) {
     // szText is 80 characters, so %d will fit
     wsprintf(pdi->szText, TEXT("%d"), g_iItemTip);
    } else {
     pdi->szText[0] = TEXT('\0');
    }
    pdi->lpszText = pdi->szText;
   }
   break;
  }
 }
 return 0;
}

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

Instead of providing fixed tooltip text, we generate it on the fly by setting the text to LPSTR_TEXTCALLBACK and producing the text in response to the TTN_GETDISPINFO notification. The technique of generating tooltip text dynamically is useful in scenarios other than this. For example, the tooltip text may change based on some state that changes often ("Back to <insert name of previous page>") or the tooltip text may be slow or expensive to compute ("Number of pages: 25"). In both cases, updating the tooltip text lazily is the correct thing to do, since it falls into the "pay for play" model: Only if the user asks for a tooltip does the program go to the extra effort of producing one.

Now that you've played with the program a bit, let's tweak it every so slightly to illustrate a point I made last time: We'll make the + and - keys add and remove colored bars. This lets you see how the tooltip code updates itself when items move around.

void
InvalidateItems(HWND hwnd, int iItemMin, int iItemMax)
{
 RECT rc;
 SetRect(&rc, 0, g_cyItem * iItemMin,
         g_cxItem, g_cyItem * iItemMax);
 InvalidateRect(hwnd, &rc, TRUE);
}

void
UpdateTooltipFromMessagePos(HWND hwnd)
{
 DWORD dwPos = GetMessagePos();
 POINT pt = { GET_X_LPARAM(dwPos),
              GET_Y_LPARAM(dwPos) };
 ScreenToClient(hwnd, &pt);
 UpdateTooltip(pt.x, pt.y);
}

void
OnChar(HWND hwnd, TCHAR ch, int cRepeat)
{
 switch (ch) {
 case TEXT('+'):
  g_cItems += cRepeat;
  InvalidateItems(hwnd, g_cItems - cRepeat, g_cItems);
  UpdateTooltipFromMessagePos(hwnd);
  break;
 case TEXT('-'):
  if (cRepeat > g_cItems) cRepeat = g_cItems;
  g_cItems -= cRepeat;
  InvalidateItems(hwnd, g_cItems, g_cItems + cRepeat);
  UpdateTooltipFromMessagePos(hwnd);
  break;
 }
}

// Add to WndProc
 HANDLE_MSG(hwnd, WM_CHAR, OnChar);

We have a few new helper functions. The first invalidates the rectangle associated with a range of items. (Conforming to Hungarian convention, the term "Max" refers to the first element outside the range. In other words, "Min/Max" is endpoint-exclusive.) Controls that manage sub-elements will almost always have a function like InvalidateItems in order to trigger a repaint when a sub-element changes its visual appearance.

The next helper function is UpdateTooltipFromMessagePos which pretty much does what it says: It takes the message position and passes those coordinates (suitably converted) to UpdateTooltip in order to keep everything in sync. Finally, the WM_CHAR handler adds or removes items based on what the user typed (taking autorepeat into account). Whenever we change the number of items, we update the tooltip because one of the items that was added or removed may have been the one beneath the cursor.

There is an important subtlety to the UpdateTooltipFromMessage function: Remember that the message position retrieved via GetMessagePos applies to the most recent message retrieved from the message queue. Messages delivered via SendMessage bypass the message queue and therefore do not update the queue message position. Once again, we see by a different means that you can't simulate input with SendMessage.

Comments (15)
  1. BryanK says:

    According to the NMTTDISPINFO reference in MSDN, the pdi->szText buffer is only 80 bytes wide (enough for 40 UCS-2/UTF-16 characters, assuming none of the characters are surrogates because you need to display one of the Unicode code-points above 65535).

    Now, here you’re only putting a maximum of 13 characters in it (the maximum value a 32-bit number can hold), so 26 bytes, but it seems a bit … iffy security-wise to rely on that.  Wouldn’t it be better to use wsnprintf (if that’s available; MSDN’s search doesn’t know what it is, and Google doesn’t know much about it)?

    Of course, maybe that would detract from the educational value of the article.  But it seems that things like that get overlooked way too often in production code, where it really does matter.  At least a note might have been good.

    (To really do it right, you probably need to malloc a buffer in lpszText, try to wsnprintf, passing the size of the buffer, then check the return value of wsnprintf to ensure it’s < the size (accounting for the possibility that one byte != one character; I don’t know the contract of wsnprintf for sure, because MSDN doesn’t have it).  If it’s equal or bigger, then the buffer’s size would be set to the return value of wsnprintf, using realloc, and then wsnprintf would be called again.  If there’s a chance of something else (e.g. another control’s WndProc or something; DoEvents in VB6 causes this kind of thing) modifying g_iItemTip between the two calls to wsnprintf, then you’ll need to make this into a loop.  Or (if the “something else” was another thread) add a lock around the entire wsnprintf / realloc / wsnprintf sequence, and around any modifications to g_iItemTip.)

    [Yes, I’m taking advantage of the fact that result fits into the provided buffer. One issue per article. -Raymond]
  2. josh says:

    BryanK: If you’re doing Win32-specific stuff anyway, you might as well use FormatMessage instead of wsnprintf.  You can even have it alloc the buffer for you.

  3. Locke says:

    Thank you for this series of tooltip posts. They are great!

    BTW.. like some others, I am not seeing a border drawn around the tooltip every time it is displayed. The first popup of the tooltip shows a border, then each successive popup does not show a border UNTIL I move the move the mouse around for an extended period of time, then hover again over an item. I see that tooltips have different delay times for reshow / initial popups (see TTM_GETDELAYTIME for info) . Maybe when the tooltip considers it a ‘reshow’ timeout, the border is not drawn for some reason?

    The follow two pictures were taken from moving to the bottom bar, then up 1 bar and back again:

    http://www.uploadfile.info/uploads/19b6a6b9c1.png

  4. BryanK says:

    One issue per article is fine, of course, but I wouldn’t be surprised if someone grabbed the code given here and used it in a production program, with a tooltip longer than 80 bytes (40 Unicode characters, assuming no surrogates).  And then got burned because of a buffer overflow.

    Just a comment in the code might be nice.  (“Beware: szText is only 80 bytes wide.” should be sufficient.)

    Regarding FormatMessage: Yeah, that might be helpful.  I don’t remember at the moment whether it can take a fixed format string, or if the format string has to be a DLL resource, but either way it’d work.  (If and only if FormatMessage is allocating the buffer, anyway.)

    [Comment added, thanks. -Raymond]
  5. Tim says:

    Thanks for the tooltip stuff – is there more to come?

    I want to know, mainly to see if you cover balloon help, and why NIN_BALLOONUSERCLICK in particular seems to be an entirely fictional event… :-)

    (at least, on my XP system it is)

  6. Dan McCarty says:

    You mentioned in a previous post that you don’t have way to store images online.  Your writeups are great, but for those of us who come here to glean a nugget of wisdom but who don’t have the time to sit down with your scratch app and compile in your sample code, it would be great if you could find a way to host an image or two so we could see the finished results.

    [Sorry, I’m a textual person. Frequently, the key point is the *interactivity*, which doesn’t come across in a screenshot anyway. And since I can’t upload pictures anyway, there’s no point arguing about it. -Raymond]
  7. Cody says:

    Off Topic: Why don’t they allow you to upload pictures?

    [I don’t have anywhere to upload them *to*. (The galleries here don’t count since people would be able to see the image before the article.) Plus I don’t want to get involved in any copyright issues. -Raymond]
  8. kokorozashi says:

    Given the title of this post, I was hoping for something about TTM_UPDATETIPTEXT. I seem to be in a situation which calls for it. My immediate question is that although it’s supposed to be for changing the text, I must pass an entire TOOLINFO. This leads me to wonder whether the other fields are being used, and if so whether this means one of the existing tools might be altered. (As I understand it, one tip may have many tools.)

  9. kokorozashi says:

    It now looks from experimentation as if it’s not necessary to fill in all the fields of a TOOLINFO  before passing it to TTM_UPDATETIPTEXT. Or, to be more scientifical about it, I’m giving TTM_UPDATETIPTEXT a NULL value for TOOLINFO.hwnd and it’s not causing any trouble even though I’ve also set TTF_IDISHWND and cleared TOOLINFO.rect. It’s a weird API, but that’s nothing new I suppose.

  10. kokorozashi says:

    In fact, if I clear all fields, then set cbSize appropriately and set lpszText to LPSTR_TEXTCALLBACK, that seems to be enough. I guess this is basically what I would have expected, but it still feels obscure and roundabout.

    [It’s not mysterious at all. All you’re updating is the text, so you need to specify (1) which tool, so the tooltip knows what text you want to replace, and (2) the new text, so the tooltip knows what you want to replace it with. And that’s what the documentation says. -Raymond]
  11. kokorozashi says:

    It may not actually be obscure and roundabout because because I’ve realized my sending a TOOLINFO full of mostly zeroes along with TTM_UPDATETIPTEXT may be having no effect at all and I may see the text change for a different reason. I need to do some more work.

  12. kokorozashi says:

    This will come as a huge shock, but of course you are right and I am completely wrong. So wrong, in fact, that it looks like I won’t even be using TTM_UPDATETIPTEXT because I want one-text-per-tip rather than -per-tool. In fact, that seems like such a good choke-point to me that I don’t know why people would want -per-tool instead. I’m sure it’s legit; I just don’t see why I would want it right now. I would much rather use LPSTR_TEXTCALLBACK and cache the text myself. So that’s what I’m doing. After I refresh my cache, I know whether the tip is visible by listening for TTN_SHOW and TTN_POP, and if it is visible, I send TTM_POP and TTM_POPUP. I figured it would flicker, and I would have to figure out what to do about that, but it doesn’t — or it flickers so fast that my eyes can’t see it. Now, go ahead, tell me how far my head is up my butt — I’ll be disappointed if it’s not. :-)

    [If you don’t have a lot of hotspots, then it’s convenient to create one tool for each hotspot. (E.g., a toolbar.) Then you just have to set them up once and let the tooltip control do all the work. If you have a lot of hotspots, then you can multiplex, as I described earlier. “Easy things should be easy; hard things should be hard.” -Raymond]
  13. James McNeill says:

    I, too, had problems with the tooltip border only appearing sporadically.  There was another problem that appears to have been related.  If you hover over the last bar so that the tooltip appears and then press + a couple of times to draw more bars, the new bars overwrite the tooltip rather than appearing underneath it.

    By comparing this code with the sample code on MSDN I experimentally determined that the WS_EX_TRANSPARENT style was the source of both quirks.  Removing it and passing 0 to the extended style makes the tooltips behave better, at least on my machine.  (I did try turning on and off various tooltip and window animation settings but got the same results regardless.)

  14. Norman Diamond says:

    > It’s not mysterious at all. All you’re

    > updating is the text

    But you’re passing an entire structure.

    In recent months, I’ve stopped experimenting to see if certain parameters can be NULL in cases where I have no useful information to point those parameters to and MSDN doesn’t say if the parameters can be null or not.  If MSDN describes a pointer and a length, the length can be zero, but if MSDN doesn’t say that the pointer can be null then I declare a byte and point the pointer to it.  Here is the reason for my change to such a rigorous practice:

    http://blogs.msdn.com/oldnewthing/archive/2006/03/20/555511.aspx

    *  All parameters must be valid.

    *  Pointers are not NULL unless explicitly

    *  permitted otherwise.

    [It’s clumsy having a TTUPDATETIPTEXT structure, a TTNEWTOOLRECT structure, a TTDELTOOL structure, a TTGETTEXT structure, etc. Instead, there’s just a generic TOOLINFO that’s used for multiple messages. -Raymond]

  15. kokorozashi says:

    I think where I got confused is that you’re explaining dynamic content in the context of multiplexing, and the "dynamic" content is really just a side effect of that multiplexing. By contrast, I need content that’s dynamic based on some external factor, say bytes arriving from the internet. It so happens that I have only a few tools, which makes things easier on me, but that’s not the critical difference. If I had thousands of potential tools, I’d still need to provide "dynamic" content in both senses mentioned here. Pile on top of this the fact that I hadn’t used this API at all before last week and my confusion compounds. Anyway, if my strategy involving LPSTR_TEXTCALLBACK, TTN_SHOW, TTN_POP, TTM_POP, and TTM_POPUP doesn’t nauseate you, I figure I’m doing OK.

Comments are closed.