Multiplexing multiple tools into one in a tooltip

The tooltip control lets you set multiple "tools" (regions of the owner window) for it to monitor. This is very convenient when the number of tools is manageably small and they don't move around much. For example, the toolbar control creates a tool for each button. But if you have hundreds or thousands of screen elements with tooltips, creating a tool for each one can be quite a lot of work, especially if the items move around a lot. For example, the listview control does not create a separate tool for each listview item, since a listview can have thousands of items, and scrolling the view results in the items moving around. Updating the tool information whenever the listview control scrolls would be extremely slow, and the work would be out of proportion to the benefit. (Updating thousands of tools on the off chance the user hovers over one of them doesn't really sit well on the cost/benefit scale.)

Instead of creating a tool for each item, you can instead multiplex all the tools into one, updating that one tool dynamically to be the one corresponding to the element the user is currently interacting with. We'll start with a fresh scratch program and create a few items which we want to give tooltips for.

int g_cItems = 10;
int g_cyItem = 20;
int g_cxItem = 200;

GetItemRect(int iItem, RECT *prc)
 SetRect(prc, 0, g_cyItem * iItem,
         g_cxItem, g_cyItem * (iItem + 1));
 return iItem >= 0 && iItem < g_cItems;

ItemHitTest(int x, int y)
 if (x < 0 || x > g_cxItem) return -1;
 if (y < 0 || y > g_cItems * g_cyItem) return -1;
 return y / g_cyItem;

PaintContent(HWND hwnd, PAINTSTRUCT *pps)
 COLORREF clrSave = GetBkColor(pps->hdc);
 for (int iItem = 0; iItem < g_cItems; iItem++) {
  RECT rc;
  GetItemRect(iItem, &rc);
  COLORREF clr = RGB((iItem & 1) ? 0x7F : 0,
                     (iItem & 2) ? 0x7F : 0,
                     (iItem & 4) ? 0x7F : 0);
  if (iItem & 8) clr *= 2;
  SetBkColor(pps->hdc, clr);
  ExtTextOut(pps->hdc, rc.left,,
             ETO_OPAQUE, &rc, TEXT(""), 0, NULL);
 SetBkColor(pps->hdc, clrSave);

We merely paint a few colored bands. To make things more interesting, you can add scroll bars. I leave you to deal with that yourself, since it would be distracting from the point here, although it would also make the sample a bit more realistic.

Next, we create a tooltip control and instead of creating a tool for each element, we create only one. For starters, it's an empty tool with no rectangle. The g_iItemTip variable tells us which item this tooltip is standing in for at any particular moment; we use -1 as a sentinel indicating that the tooltip is not active.

HWND g_hwndTT;
int g_iItemTip;

OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
                           0, 0, 0, 0,
                           hwnd, NULL, g_hinst, NULL);
 if (!g_hwndTT) return FALSE;

 g_iItemTip = -1;
 TOOLINFO ti = { sizeof(ti) };
 ti.hwnd = hwnd;
 ti.uId = 0;
 ti.lpszText = TEXT("Placeholder tooltip");
 SendMessage(g_hwndTT, TTM_ADDTOOL, 0, (LPARAM)&ti);
 return TRUE;

You may have noticed that we do not use the TTF_SUBCLASS flag in our tool. We'll see why later.

The single tool for the tooltip covers our entire client rectangle. We maintain this property as the window resizes.

OnSize(HWND hwnd, UINT state, int cx, int cy)
 TOOLINFO ti = { sizeof(ti) };
 ti.hwnd = hwnd;
 ti.uId = 0;
 GetClientRect(hwnd, &ti.rect);
 SendMessage(g_hwndTT, TTM_NEWTOOLRECT, 0, (LPARAM)&ti);

We need to keep the g_iItemTip up to date so we know which item our tooltip is standing for at any particular moment. That is done by the UpdateTooltip function:

UpdateTooltip(int x, int y)
 int iItemOld = g_iItemTip;
 g_iItemTip = ItemHitTest(x, y);
 if (iItemOld != g_iItemTip) {
   SendMessage(g_hwndTT, TTM_POP, 0, 0);

To update the tooltip, we check whether the mouse is over the same item as it was last time. If not, then we update our "Which item is under the mouse now?" variable and pop the old bubble (if any). And we always relay the message to the tooltip so it can do its tooltip thing. This function also explains why we did not use the TTF_SUBCLASS flag when we created our tool: We need to do some processing before the tooltip. If we had allowed the tooltip to subclass, then it would process the mouse message first, which means that our TTM_POP would have popped the new updated tooltip instead of the stale old tooltip.

This UpdateTooltip function is very important. It must be called any time the mouse may be hovering over a different item. This could be because the mouse moved or because the items under the mouse changed positions. I don't have any scrolling in this example, but if I did, then you would see a call to UpdateTooltip whenever we updated the scroll origin point because the act of scrolling may have moved the item that was under the mouse. (Failing to maintain mouse state after a scrolling operation is a common programming oversight.) Furthermore, if items were added or deleted dynamically, then a call to UpdateTooltip would have to be made once an item was added or deleted because the added or deleted item might be the one under the mouse.

The easy one to take care of is the mouse motion:

RelayEvent(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
 UpdateTooltip(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
 MSG msg;
 msg.hwnd = hwnd;
 msg.message = uiMsg;
 msg.wParam = wParam;
 msg.lParam = lParam;
 SendMessage(g_hwndTT, TTM_RELAYEVENT, 0, (LPARAM)&msg);

WndProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
 if ((uiMsg >= WM_MOUSEFIRST && uiMsg <= WM_MOUSELAST) ||
     uiMsg == WM_NCMOUSEMOVE) {
  RelayEvent(hwnd, uiMsg, wParam, lParam);

 switch (uiMsg) {
  ... as before ...

If we get a mouse message, then the RelayEvent message updates our tooltip state and then relays the message to the tooltip. See the discussion above for the importance of doing this in the right order.

You can run the program now. Observe that the program acts as if each colored band has its own tooltip, even though there is really only one tooltip that we keep recycling.

We're still not done. The tooltip text is the same for each item, which is unrealistic for a real program. We'll address this next time.

Comments (8)
  1. Anders says:

    Don’t know why this blog is so light on comments lately, summer might have something to do with it I don’t know, but I still read it every day, so keep up the good work Raymond

  2. bvleur says:

    I guess Raymond can check the number of readers without them having to comment (if he even cares about his popularity). I’m reading and enjoying this blog every day just to learn some nice details about windows although I actually never write code a this level.

    The tiny details makes this interesting (as the concept is really easy) like:

    1. Why do you draw the color bands with empty text and not some kind FillRectangle function?

    2. Why does the tools rect change and is bigger than all of your items? I expected it to be set to the size of all your items (so just (0, 0, g_cxItem, g_cItems & g_cyItem)).

    3. How does the tooltip disappear now that you won’t relay a mouse message that it’s outside of the item-rect? (It will POP because of itemTip becoming -1, but it will reappear as it’s still in the client-rect right?)

    4. You say you can’t subclass because you would POP the new tooltip. How could there have been a new tooltip if you haven’t changed the tool you’re over? Could you have sent a POPUP when UpdateTooltip detects you’re over a new item to make the new bubble appear as soon as you’re over a new item?

    [(1) I’ll talk more about using ExtTextOut for solid fills later. (2) Using a tighter tool rectangle just re-creates the problem we’re trying to solve! (3) Run the program and find out. (4a) By “the new tooltip” I mean “the tooltip with the new text”. (4b) That would be a change in behavior. There would be no tooltip delay. -Raymond]
  3. AC says:

    I’m still observing the same effects:

    Even without TTS_NOANIMATE, sometimes the tooltip is drawn without animation and border (at least I believe it’s a tooltip since it’s yellow). And with that flag, it’s always without animation and border. But whenever it’s drawn without border, the window is clipped to the parent (when I resize the partent window).

    Is it possible to eliminate animation and still have tooltip that is not clipped? The real “big” apps (i.e. IE :) ) don’t have such problems.

    [Maybe you disabled tooltip animations in the control panel? I don’t know. -Raymond]
  4. Martin Filteau says:

    Why are you creating the tooltip with WS_EX_TRANSPARENT and not WS_EX_TOPMOST ?

    [Try it without WS_EX_TRANSPARENT. And there’s no need to be WS_EX_TOPMOST here. One thing that bugs me is topmost windows that don’t need to be. -Raymond]
  5. KiwiBlue says:

    bvleur: ExtTextOut is faster than creating brush and passing it to FillRect (MFC uses it for its CDC::FillSolidRect method). I vaguely remember reading that it’s also faster than MoveTo/LineTo for horizontal/vertical lines.

    Not that it should matter in such simple app anyway :)

  6. Norman Diamond says:

    And there’s no need to be WS_EX_TOPMOST

    > here. One thing that bugs me is topmost

    > windows that don’t need to be. -Raymond]

    Today for me, buttons in the task bar are showing tooltips more topmost than the task bar itself.  Last week for me, buttons in the task bar were showing tooltips more than half obscured by the task bar itself.  I did not change any settings and did not receive any Windows Updates during this time.  How can we figure out if tooltip windows need WS_EX_TOPMOST or not, and how can we figure out if WS_EX_TOPMOST will even work for them or not?

Comments are closed.