June, 2006

  • The Old New Thing

    Congratulations, Montréal, on finally paying for the 1976 Olympic Games

    • 11 Comments

    Assuming everything went according to plan, Montréal today made the final payment on Olympic Stadium (or Le Stade Olympique), originally built for the 1976 Olympic Games, and nicknamed "The Big O" (or "The Big Owe", if you prefer). Before you snicker, at least you have to give them credit for keeping the stadium until it was paid off. The Seattle Kingdome opened just a few months before Montréal's Olympic Stadium, but we demolished it in 2000 even though we will still be paying for it until 2012.

  • The Old New Thing

    2006 mid-year link clearance

    • 8 Comments

    A few random links that I've collected.

  • The Old New Thing

    Generating tooltip text dynamically

    • 15 Comments

    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.

  • The Old New Thing

    Multiplexing multiple tools into one in a tooltip

    • 8 Comments

    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;
    
    BOOL
    GetItemRect(int iItem, RECT *prc)
    {
     SetRect(prc, 0, g_cyItem * iItem,
             g_cxItem, g_cyItem * (iItem + 1));
     return iItem >= 0 && iItem < g_cItems;
    }
    
    int
    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;
    }
    
    void
    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, rc.top,
                 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;
    
    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_iItemTip = -1;
     TOOLINFO ti = { sizeof(ti) };
     ti.uFlags = TTF_TRANSPARENT;
     ti.hwnd = hwnd;
     ti.uId = 0;
     ti.lpszText = TEXT("Placeholder tooltip");
     SetRectEmpty(&ti.rect);
     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.

    void
    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:

    void
    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:

    void
    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);
    }
    
    LRESULT CALLBACK
    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.

  • The Old New Thing

    ... so it doesn't poop all over your office

    • 14 Comments

    The other day I caught a fragment of a conversation, namely somebody concluding a sentence with the clause "... so it doesn't poop all over your office".

    I don't know what they were talking about and I'm not sure I want to find out.

    (Ry Jones thinks I was the victim a game of tomato funeral. I'm not convinced; it was a conversation that I had been drifting in and out of at the lunch table. I just picked a bad time to drift back in.)

  • The Old New Thing

    Using custom-draw in tooltips to adjust the font

    • 2 Comments

    Last time, we looked at in-place tooltips. In that example, we finessed the font problem by simply setting the destination font into the tooltip control. We got away with that since we had only one tool. But if you have multiple tools with different fonts, then you can't set a font into the tooltip control and expect it to work for every tool. That's where custom draw comes in.

    Start with the program from last time, but this time, we'll set the font via custom-draw instead of setting it globally.

    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
     ...
     // SetWindowFont(g_hwndTT, g_hfTT, FALSE);
     ...
    }
    
    LRESULT
    OnTooltipCustomDraw(HWND hwnd, NMHDR *pnm)
    {
     LPNMTTCUSTOMDRAW pcd = (LPNMTTCUSTOMDRAW)pnm;
     if (pcd->nmcd.dwDrawStage == CDDS_PREPAINT) {
      SelectFont(pcd->nmcd.hdc, g_hfTT);
      return CDRF_NEWFONT;
     }
     return 0;
    }
    
    LRESULT
    OnNotify(HWND hwnd, int idFrom, NMHDR *pnm)
    {
     if (pnm->hwndFrom == g_hwndTT) {
      switch (pnm->code) {
      case NM_CUSTOMDRAW:
       return OnTooltipCustomDraw(hwnd, pnm);
      case TTN_SHOW:
       return OnTooltipShow(hwnd, pnm);
      }
     }
     return 0;
    }
    

    Of course, doing this is overkill in our case where we have only one tool, so you'll have to imagine that the tooltip is managing multiple tool regions, each with a different font. When we get the NM_CUSTOMDRAW notification, we respond to the CDDS_PREPAINT stage by changing the font and returning the CDRF_NEWFONT flag (which is necessary when changing the font).

  • The Old New Thing

    Look who bought my name

    • 36 Comments

    Commenter Pavel Vozenilek noticed that if you type my name into Google, there is only one sponsored link, and it's from Google themselves, inviting you to apply for a job there. (Maybe I should sue. Perhaps I can get a settlement.)

    When I mentioned this to some other people at Microsoft, they started hunting around to see what sort of ads came up on Google when they searched for other names. Eric Gunnerson and Brad Abrams are available at low prices, and you can get Randy Holloway on eBay. Of all the Microsoft bloggers, the only other one that gets the Google Jobs treatment is Larry Osterman.

    (Note: All links behaved as described when I originally wrote this entry; due to the nature of sponsored links, they may behave differently by the time you read this.)

    [9am update: Wow that was fast. The Google Jobs links for Larry Osterman and me are already gone.]

  • The Old New Thing

    Coding in-place tooltips

    • 17 Comments

    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.

  • The Old New Thing

    The subtle usability considerations of conference nametags

    • 44 Comments

    When you go to a conference or some other event where everybody wears a nametag, pay closer attention to the nametag design. There are many subtle usability mistakes that I see far too often.

    First of all, is your name easy to read? It's called a nametag, after all; the name and affiliation of the wearer should be the most prominent thing on the tag. I've been to events where the most prominent thing on the nametag was the name of the conference itself. Hey, everybody presumably already knows what conference they're attending. It's printed on the agenda sheet, it's printed on the tote bag, it's printed on every sign at the venue, it's even printed on the pens you gave out, for goodness' sake. Tell them something they don't know: Who they are talking to. (Corollary: Don't put the name at the bottom of the tag.)

    Okay, now that you've got the name on the nametag in a position and style where it actually serves its purpose, you have to make sure the tag is visible when worn. Most computer events use a lanyard-style nametag. If the lanyard length is not adjustable, then you have a new problem: You have to make the cord long enough to go around the wearer's head. But once you do that, the cord is now so long that the nametag itself hangs around the wearer's belly-button. This is already awkward enough, but if the conference entails sit-down meetings, the nametag will end up into the wearer's lap. And if you have the meetings at tables, the nametag will disappear beneath the surface of table. A nametag that you can't see isn't doing its job.

    (Diagrams require a VML-enabled browser.)

    Can flip

    Stable

    Great, you have a name on the nametag that people can see, you are keeping the tag visible, you think you're home free. But wait, how is your nametag mounted to the lanyard? Nearly all lanyard nametags I've seen are mounted from a single clip or hole at the top center. With this design, the nametag can easily flip around, pushing the person's name into their chest and showing the nametag's backside to the rest of the world. One solution to this problem is to make the nametag reversible, so that even if it flips, the name is still visible. Another solution is to mount the nametag from two holes, one in each top corner. In this manner, the nametag becomes flip-resistant.


    Just a few little details in nametag design. But you'd be surprised how many people miss them. (The PDC nametags are the only one in recent memory that addressed all three problems.)

  • The Old New Thing

    The continuing phenomenon of size inflation in fast food

    • 67 Comments

    Wendy's is getting rid of "Biggie" and "Great Biggie" size drinks and fries from their menu. Oh, they're still offering them, just with a different name. What used to be "Biggie" is now "medium" and what used to be "Great Biggie" is now "large". Even the "small" drink is a massive 20 ounces, or two and a half FDA servings.

Page 1 of 4 (34 items) 1234