June, 2006

  • The Old New Thing

    Pitfalls of transparent rendering of anti-aliased fonts

    • 41 Comments

    Windows provides a variety of technologies for rendering monochrome text on color displays, taking advantage of display characteristics to provide smoother results. These include grayscale anti-aliasing as well as the more advanced ClearType technique. Both of these methods read from the background pixels to decide what pixels to draw in the foreground. This means that rendering text requires extra attention.

    If you draw text with an opaque background, there is no problem because you are explicitly drawing the background pixels as part of the text-drawing call, so the results are consistent regardless of what the previous background pixels were. But if you draw text with a transparent background, then you must make sure the background pixels that you draw against are the ones you really want.

    The most common way people mess this up is by drawing text multiple times. I've seen programs which draw text darker and darker the longer you use it. We'll see here how this can happen and what you need to do to avoid it. Start with the scratch program and make these changes:

    HFONT g_hfAntialias;
    HFONT g_hfClearType;
    
    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
     g_hfAntialias = CreateFont(-20, 0, 0, 0, FW_NORMAL, 0, 0, 0,
        DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
        ANTIALIASED_QUALITY, DEFAULT_PITCH, TEXT("Tahoma"));
     g_hfClearType = CreateFont(-20, 0, 0, 0, FW_NORMAL, 0, 0, 0,
        DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
        CLEARTYPE_QUALITY, DEFAULT_PITCH, TEXT("Tahoma"));
     return g_hfAntialias && g_hfClearType;
    }
    
    void
    OnDestroy(HWND hwnd)
    {
     if (g_hfAntialias) DeleteObject(g_hfAntialias);
     if (g_hfClearType) DeleteObject(g_hfClearType);
     PostQuitMessage(0);
    }
    
    
    void MultiPaint(HDC hdc, int x, int y, int n)
    {
     LPCTSTR psz = TEXT("The quick brown fox jumps over the lazy dog.");
     int cch = lstrlen(psz);
     for (int i = 0; i < n; i++) {
       TextOut(hdc, x, y, psz, cch);
     }
    }
    
    void
    PaintContent(HWND hwnd, PAINTSTRUCT *pps)
    {
     int iModePrev = SetBkMode(pps->hdc, TRANSPARENT);
     HFONT hfPrev = SelectFont(pps->hdc, g_hfAntialias);
     MultiPaint(pps->hdc, 10,  0, 1);
     MultiPaint(pps->hdc, 10, 20, 2);
     MultiPaint(pps->hdc, 10, 40, 3);
     SelectFont(pps->hdc, g_hfClearType);
     MultiPaint(pps->hdc, 10, 80, 1);
     MultiPaint(pps->hdc, 10,100, 2);
     MultiPaint(pps->hdc, 10,120, 3);
     SelectFont(pps->hdc, hfPrev);
     SetBkMode(pps->hdc, iModePrev);
    }
    

    This program creates two fonts, one with anti-aliased (grayscale) quality and another with ClearType quality. (I have no idea why people claim that there is no thread-safe way to enable ClearType on an individual basis. We're doing it just fine here.)

    Run this program and take a close look at the results. Observe that in each set of three rows of text, the more times we overprint, the darker the text. In particular, notice that overprinting the anti-aliased font makes the result significantly uglier and uglier!

    What went wrong?

    The first time we drew the text, the background was a solid fill of the window background color. But when the text is drawn over itself, the background it sees is the previous text output. When the algorithm decides that "This pixel should be drawn by making the existing pixel 50% darker," it actually comes out 75% darker since the pixel is darkened twice. And if you draw it three times, the pixel comes out 88% darker.

    When you draw text, draw it exactly once, and draw it over the background you ultimately want. This allows the anti-aliasing and ClearType engines to perform their work with accurate information.

    The programs that darken the text are falling afoul of the overprinting problem. When the programs decide that some screen content needs to be redrawn (for example, if the focus rectangle needs to be added or removed), they "save time" by refraining from erasing the background and merely drawing the text again (but with/without the focus rectangle). Unfortunately, if you don't erase the background, then the text ends up drawn over a previous copy of itself, resulting in darkening.

    The solution is to draw text over the correct background. If you don't know what background is on the screen right now, then you need to erase it in order to set it to a known state. Otherwise, you will be blending text against an unknown quantity, which leads to inconsistent (and ugly) results.

    If you keep your eagle eyes open, you can often spot another case where people make the overprinting mistake: When text in a control (say, a check box) becomes darker and darker the more times you tab through it. This happens when programs don't pay close attention to the flags passed in the DRAWITEMSTRUCT that is passed to the WM_DRAWITEM message. For example, some people simply draw the entire item in response to the WM_DRAWITEM message, even though the window manager passed the ODA_FOCUS flag, indicating that you should only draw or erase the focus rectangle. This is not a problem if drawing the entire item includes erasing the background, but if you assume that the WM_ERASEBKGND message had erased the background, then you will end up overprinting your text in the case where you were asked only to draw the focus rectangle. In that case, the control is not erased; all you have to do is draw the focus rectangle. If you also draw the text, you are doing what the MultiPaint function did: Drawing text over text, and the result is text that gets darker each time it repaints.

  • The Old New Thing

    Lies and statistics: 600,000 Chinese engineers

    • 43 Comments

    Everybody "knows" that China produced 600,000 engineers in 2004 (as compared to 70,000 in the United States), but Carl Bialik at the Wall Street Journal [corrected 9:30am] smelled something funny, so he chased the source of the numbers to see whether this "fact" was indeed true. It wasn't. NPR interviewed a Duke professor whose class undertook their own investigation of these bogus numbers. The US number is really closer to 140,000. The count of Indian engineers? Nobody really knows. Deans of universities don't even know how many colleges belong to their university! China was even stranger.

    The Chinese central government in Beijing had simply decided that 600,000 is the number of engineers they want China to graduate each year. "The government has told the provinces that they have to graduate more engineers, so the provinces tell the government what they want to hear."

    If the central government decrees that the country will produce 600,000 engineers, then by golly, that's what the official statistics will say, what a surprise.

  • The Old New Thing

    Fumbling around in the dark and stumbling across the wrong solution

    • 68 Comments

    I don't mean to pick on this series of entries, but it illustrates an interesting pattern of stumbling across the wrong "solution".

    The series begins by attempting to trigger the system's monitor blank timeout by posting a message to the desktop window. As we saw earlier, the desktop window is a very special window and as a rule should be avoided, since it won't behave like windows created by applications. In particular, the author tried to post a message to the desktop window. This used to work in the historically open world of the window manager, but security and robustness concerns have come to take priority over compatibility. In Windows XP SP2, the desktop window resists being disabled because programs were doing it inadvertently, and it appears that the desktop also resists having messages posted to it. My guess is that this was done as a way to strengthen protection against shatter attacks. This did improve robustness and stability, but it also broke the article's dubious PostMessage hack.

    Enter round three, wherein the author fumbled around for other windows the monitor blank timeout message could be posted to, and eventually the author found that posting the message to the mysterious window HWND_TOPMOST = -1 seemed to do the trick.

    I knew in the back of my mind that people developed software this way, but the hopeful part of my brain continued to wish that it was merely taking place in a fantasy world. Making up intentionally invalid parameters and seeing what happens falls into the category of malicious goofing around, not in the realm of software engineering and design. Even if you find something that seems to work, you certainly wouldn't design a product around it!

    (Similarly, I've seen people ask questions like "What does message 49251 mean?" This is the reverse case: Seeing a made-up number and attempting to assign meaning to it. Message numbers starting at 0xC000 (decimal 49152) are messages registered via RegisterWindowMessage. The numerical value of the message associated with a registered window message is unpredictable and varies from desktop to desktop. The only guarantee is that it will remain consistent within a single desktop.)

    If you look more carefully at what the author stumbled across, you'll see that the "solution" is actually another bug. It so happens that the numerical value -1 for a window handle is suspiciously close to the value of HWND_BROADCAST:

    #define HWND_BROADCAST  ((HWND)0xffff)
    

    It so happens that internally, the window manager supports (HWND)-1 as an alternative value for HWND_BROADCAST. (I leave you to speculate why.) As a result, what the author actually is doing is broadcasting the monitor power-off message to all top-level windows! As we saw before, broadcasting messages is a very dangerous business, and in this case, the author is just lucky that all the windows on the desktop interpret the message the same way, that it is safe to process the message multiple times, and none of the windows perform any special filtering for that message. (Another author stumbled across the same incorrect "solution" but didn't provide any insight into the process by which the result was arrived at. Yet another author sort of found some issues but didn't quite put them all together.)

    For example, a presentation program might want to suppress monitor power-off when it is the foreground window by trapping the message and turning the monitor back on. If such a program happens to be running, broadcasting the power-off message to all top-level windows would turn off the monitor for all the windows that deferred to system default behavior, but when that presentation program received the message, it would turn the monitor back on. Now you're at the mercy of the order in which the windows process that broadcast message. When the presentation program processes the message, the monitor will turn back on, and if that program happens to be the last one to process the message (say, it got paged out and was slow to page back in), then the monitor will merely blink off and back on.

    The correct solution is not to post messages to random windows. If you want the message to go through window message default processing, create a window and process it yourself. Don't try to trick some other window (or in this case, hundreds of other windows simultaneously) into doing it for you.

  • The Old New Thing

    Remember what happens when you broadcast a message

    • 20 Comments

    Occasionally I catch people doing things like broadcasting a WM_COMMAND message to all top-level windows. This is one of those things that is so obviously wrong I don't see how people even thought to try it in the first place.

    Suppose you broadcast the message

    SendMessage(HWND_BROADCAST, WM_COMMAND, 100, 0);
    

    What happens?

    Every top-level window receives the message with the same parameters, and every top-level window starts interpreting those parameters in their own idiosyncratic way. As you know (since you've written them yourself), each window procedure defines its own menu items and child windows and there is no guarantee that command 100 will mean the same thing to each window. A dialog box with the template

    #define IDC_USEDEFAULT 100
    ...
        AUTORADIOBUTTON "Use &default color",
                        IDC_USEDEFAULT, 14, 38, 68, 10, WS_TABSTOP
    

    would interpret the message as

    id IDC_USEDEFAULT (100)
    command BN_CLICKED (0)
    window NULL (0) — illegal parameter

    Depending on how the dialog procedure is written, it might try to send a message back to the button control (and fail since you passed NULL as the window handle), or it might update some dialog state like disabling the color customization controls (since it was told that the user clicked the "User default color" radio button).

    Another dialog box might have the template

    #define IDC_CHANGE 100
    ...
        PUSHBUTTON      "C&hange", IDC_CHANGE, 88, 95, 50, 14
    

    This dialog procedure would interpret the message as

    id IDC_CHANGE (100)
    command BN_CLICKED (0)
    window NULL (0) — illegal parameter

    The reaction would probably be to apply the changes that were pending in the dialog.

    Meanwhile, another window might have a menu that goes like this:

    #define IDC_REFRESH 100
    ...
            MENUITEM "&Refresh", IDC_REFRESH
    

    It is going to interpret the message as the user having selected "Refresh" from the window menu.

    id IDC_REFRESH (100)
    command 0 — illegal parameter, must be 1 for menu items
    window NULL (0)

    Not only is the command code invalid for a menu item, the window might be in a state where the program had disabled the "Refresh" option. Yet you sent the window a message as if to say that the user selected it anyway, which is impossible. Congratulations, you just presented the program with an impossible situation and it very well may crash as a result. For example, the program may have disabled the "Refresh" option since there is no current object to refresh. When you send it the "Refresh" command, it will try to refresh the current object and crash with a null pointer error.

    Obviously, then, you cannot broadcast the WM_COMMAND message since there is no universal meaning for any of the command IDs. A command ID that means "Refresh" to one window might mean "Change" to another.

    The same logic applies to nearly all of the standard Windows messages. The ones that are actually designed to be broadcast are as follows:

    WM_SYSCOLORCHANGE
    WM_SETTINGCHANGE (= WM_WININICHANGE)
    WM_DEVMODECHANGE
    WM_FONTCHANGE
    WM_TIMECHANGE
    WM_DDE_INITIATE

    If you try to broadcast a message in the WM_USER or WM_APP ranges, then you're even crazier than I thought. As we've already seen, the meaning of window messages in those ranges are defined by the window class or the application that created the window. Not only are the parameters to the message context-sensitive, the message itself is! This means that sending a random window a WM_USER+1 message (say) will result in extremely random behavior. (We saw this before in the context of broadcasts, but it applies to directed delivery, too.) If it's a dialog box, it will think you sent a DM_SETDEFID message, and you just changed that dialog's default ID. If it's a common dialog box, it will think you sent a WM_CHOOSEFONT_GETLOGFONT message, and if you're lucky, it will crash trying to return the LOGFONT through an invalid pointer. (If you're not lucky, the parameter you passed will happen to be a valid pointer and the program will merely corrupt its own memory in some strange way, only to behave erratically later on.) If it's a tooltip control, then you just sent it the TTM_ACTIVATE message and you just manipulated the tooltip's activation state.

    The same caution applies, using the same logic, to sending messages without universal meaning to windows whose window class you do not have an interface contract with. For example, I'll see people sending the PSM_PRESSBUTTON message to a window on the blind-faith assumption that it is a property sheet.

    Remember, then, that when you send a message to a window, you need to be sure that the window will interpret it in the manner you intend.

  • The Old New Thing

    What happened to the traffic circle at the corner of 156th Ave NE and NE 56th Way?

    • 57 Comments

    Windows Live Local and Google Maps both show a traffic circle at the corner of 156th Ave NE and NE 56th Way, but if you pay the intersection a visit in person, you won't find one. It was replaced with a speed bump in 2005. Why?

    I stumbled across the explanation completely by happenstance. There was a small article in the local newspaper that described an accident that occurred elsewhere in Redmond at a traffic circle. A car was driving down the street in excess of the speed limit and failed to negotiate the circle, resulting in the car going off the road. In the flurry of legal action that ensued, somehow the City of Redmond ended up being held responsible for creating "dangerous driving conditions" or something like that. As a result, the City of Redmond went around removing all the city's traffic circles and replacing them with speed bumps.

    Apparently, in this country, a city can be held responsible for conditions that are dangerous to people who are willfully violating the law by exceeding the posted maximum safe speed.

    Minor league baseball team the Altoona Curve announced a Salute to Frivolous Lawsuit Night promotion for their game on July 2, though it may be that their lawyers subsequently advised against it, since it doesn't appear on their official list of promotions... Perhaps they feared a frivolous lawsuit.

  • The Old New Thing

    Why did the Add or Remove Programs control panel try to guess all that information?

    • 38 Comments

    As we saw earlier, the "Add or Remove Programs" control panel used several heuristics to attempt to determine things like program size and frequency of user. Why did it bother doing this at all?

    At the time the feature was added, disk space was not cheap like it is today. One of the problems users were having was running out of disk space and not knowing what they could safely delete. Thus was born the Disk Cleanup utility, which attempted to guide the user through various things that could be deleted in order to make disk space available.

    In addition to cleaning up temporary files, you could also remove programs that weren't being used. But how do you know which programs you weren't using? (Maybe you were using a program without realizing it because it ran automatically.) And how do you know how much disk space would be recovered if you removed a program? That's where the program size and frequency of use heuristics came in. By providing this information (or at least trying to), the "Add or Remove Programs" control panel could help users decide which programs to remove.

    Of course, nowadays, with hard drives in the hundreds-of-gigabytes range, disk space has become so cheap as to be nearly free. The need to remove programs to make more disk space available is largely gone, but the feature remains as a vestigial organ.

  • The Old New Thing

    The forgotten common controls: The MenuHelp function

    • 4 Comments

    The MenuHelp function is one of the more confusing ones in the common controls library. Fortunately, you will almost certainly never had need to use it, and once you learn the history of the MenuHelp function, you won't want to use it anyway.

    Our story begins with 16-bit Windows. The WM_MENUSELECT message is sent to notify a window of changes in the selection state of a menu that has been associated with the window, either by virtue of being the window's menu bar or by having been passed as the owner window to a function like TrackPopupMenu. The parameters to the WM_MENUSELECT message in 16-bit Windows were as follows:

    wParam  menu item ID  if selection is a plain menu item
    pop-up menu handle  if selection is a pop-up menu
    lParam  MAKELPARAM(flags, parent menu handle) 

    The MenuHelp function parsed the parameters of the WM_MENUSELECT message in conjunction with a table describing the mapping between menu items and help strings, displaying the selected string in the status bar. The information was provided in the confusing format of an array of UINTs that took the following format (expressed in pseudo-C):

    struct MENUHELPPOPUPUINTS {
     UINT uiPopupStringID;
     HMENU hmenuPopup;
    };
    struct MENUHELPUINTS {
     UINT uiMenuItemIDStringOffset;
     UINT uiMenuIndexStringOffset;
     MENUHELPPOPUPUINTS rgwPopups[];
    };
    

    The uiMenuItemIDStringOffset specifies the value to add to the menu ID to convert it to a string ID that is to be displayed in the status bar. For example, if you had

        MENUITEM "&New\tCtrl+N"    ,200
    

    in your menu template, and you specified an offset of 1000, then the MenuHelp function will look for the help string as string identifier 200 + 1000 = 1200:

    STRINGTABLE BEGIN
    1200 "Opens a new blank document."
    END
    

    The uiMenuIndexStringOffset does the same thing for pop-up menus that were direct children of the main menu, but since pop-up menus in 16-bit Windows didn't have IDs, the zero-based menu index was used instead. For example, if your menu had the top-level structure

    BEGIN
      POPUP "&File"
      BEGIN
      ...
      END
      POPUP "&View"
      BEGIN
      ...
      END
    END
    

    and you specified a uiMenuIndexStringOffset of 800, then the string for the File menu was expected to be at 0 + 800 = 800 and the string for the View menu was expected at 1 + 800 = 801.

    STRINGTABLE BEGIN
    800 "Contains commands for working with the current document."
    801 "Contains edit commands."
    END
    

    The last case is a pop-up menu that is a grandchild (or deeper descendant) of the main menu. As we saw above, the WM_MENUSELECT message encoded the handle of the pop-up menu rather than its ID. This handle was looked up in the variable-length array of MENUHELPPOPUPUINTS elements (terminated by a {0, 0} entry). Notice that the second member of the MENUHELPPOPUPUINTS structure is an HMENU rather than a UINT. But in 16-bit Windows, sizeof(HMENU) == sizeof(UINT) == 2, and 16-bit code (such as the WM_MENUSELECT message) relied heavily on coincidences like this.

    If a pop-up window had the handle, say, (HMENU)0x1234, the MenuHelp function would look for a MENUHELPPOPUPUINTS entry which had a hMenuPopup equal to (HMENU)0x1234, at which point it would use the corresponding uiPopupStringID as the help string.

    Let's take a look at one of these in practice. Here's a menu and a corresponding string table:

    1 MENU
    BEGIN
      POPUP "&File"
      BEGIN
        MENUITEM "&New\tCtrl+N"    ,200
        MENUITEM "&Open\tCtrl+O"   ,201
        MENUITEM "&Save\tCtrl+S"   ,202
        MENUITEM "Save &As"        ,203
        MENUITEM ""                ,-1
        MENUITEM "E&xit"           ,204
      END
    
      POPUP "&View"
      BEGIN
        MENUITEM "&Status bar"     ,240
        MENUITEM "&Full screen"    ,230
        POPUP "Te&xt Size"
        BEGIN
          MENUITEM "&Large"        ,225
          MENUITEM "&Normal"       ,226
          MENUITEM "&Small"        ,227
        END
      END
    END
    
    STRINGTABLE BEGIN
     800 "Contains commands for loading and saving files."
     801 "Contains commands for manipulating the view."
    1200 "Opens a new blank document."
    1201 "Opens an existing document."
    1202 "Saves the current document."
    1203 "Saves the current document with a new name."
    1225 "Selects large font size."
    1226 "Selects normal font size."
    1227 "Selects small font size."
    1230 "Maximizes the window to full screen."
    1240 "Shows or hides the status bar."
    2006 "Specifies the relative size of text."
    END
    

    Notice that there was no requirement that the menu item identifiers be consecutive. All that the MenuHelp function cared about is that the relationship between the menu item identifiers and the help strings was in the form of a simple offset.

    The table that connects the menu to the string table goes like this:

    UINT rguiHelp[] = {
      1000,    // uiMenuItemIDStringOffset
       800,    // uiMenuIndexStringOffset
      2006, 0, // uiPopupStringID, placeholder
         0, 0  // end of MENUHELPPOPUPUINTS
    };
    

    Since there is a grandchild pop-up menu, we created a placeholder entry that will be filled in with the menu handle at run time:

    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
      HMENU hmenuMain = GetMenu(hwnd);
      HMENU hmenuView = GetSubMenu(hmenuMain, 1);
      HMENU hmenuText = GetSubMenu(hmenuView, 2);
      rguiHelp[3] = (UINT)hmenuText;
    
      g_hwndStatus = CreateWindow(STATUSCLASSNAME, NULL,
            WS_CHILD | CCS_BOTTOM | SBARS_SIZEGRIP | WS_VISIBLE,
            0, 0, 0, 0, hwnd, (HMENU)100, g_hinst, 0);
    
      return g_hwndStatus != NULL;
    }
    

    We locate the "Text Size" menu and put its menu handle into the rguiHelp array so that the MenuHelp command can find it. The window procedure would then include the line:

    ...
        case WM_MENUSELECT:
          MenuHelp(uiMsg, wParam, lParam, GetMenu(hwnd),
                   g_hinst, g_hwndStatus, rguiHelp);
          break;
    ...
    

    That last step finally connects all the pieces. When the WM_MENUSELECT message arrives, the MenuHelp function looks at the item that was selected, uses it to look up the appropriate string resource, loads the resource from the provided HINSTANCE and displays it in the status bar.

    To make the sample complete, we need to do a little extra bookkeeping:

    HWND g_hwndStatus;
    
    void
    OnSize(HWND hwnd, UINT state, int cx, int cy)
    {
      MoveWindow(g_hwndStatus, 0, 0, cx, cy, TRUE);
    }
    
    // change to InitApp
        wc.lpszMenuName = MAKEINTRESOURCE(1);
    

    (I'd invite you to code up this sample 16-bit program and run it, but I doubt anybody would be able to take me up on the invitation since very few people have access to a 16-bit compiler for Windows any more.)

    This method works great for 16-bit code. But look at what happened during the transition to 32-bit Windows: The parameters to the WM_MENUSELECT message had to change since menu handles are 32-bit values. There was no room in two 32-bit window message parameters to shove 48 bits of data (two window handles and 16 bits of flags). Something had to give, and what gave was the pop-up menu handle. Instead of passing the handle, the index of the pop-up menu was passed in the message parameters. This did not result in any loss of data since the menu handle could be recovered by passing the parent menu handle and the pop-up menu index to the GetSubMenu function. The repacking of the parameters thus goes like this:

    LOWORD(wParam)  menu item ID  if selection is a plain menu item
      pop-up menu index  if selection is a pop-up menu
    HIWORD(wParam)  flags 
    lParam  parent menu handle 

    The array of UINTs therefore changed its meaning to match the new message packing:

    struct MENUHELPPOPUPUINTS {
     UINT uiPopupStringID;
     UINT uiPopupIndex;
    };
    struct MENUHELPUINTS {
     UINT uiMenuItemIDStringOffset;
     UINT uiMenuIndexStringOffset;
     MENUHELPPOPUPUINTS rgwPopups[];
    };
    

    The advantage of changing the value from an HMENU to a UINT index is that the array does not need to be modified at run time. Okay, let's actually try this. Start with the scratch program, attach the resources I gave above, and use the following help array and code:

    UINT rguiHelp[] = {
      1000,    // uiMenuItemIDStringOffset
       800,    // uiMenuIndexStringOffset
      2006, 2, // uiPopupStringID, uiPopupMenuIndex
      0,0      // end of MENUHELPPOPUPUINTS
    };
    
    HWND g_hwndStatus;
    
    void
    OnSize(HWND hwnd, UINT state, int cx, int cy)
    {
      MoveWindow(g_hwndStatus, 0, 0, cx, cy, TRUE);
    }
    
    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
      g_hwndStatus = CreateWindow(STATUSCLASSNAME, NULL,
            WS_CHILD | CCS_BOTTOM | SBARS_SIZEGRIP | WS_VISIBLE,
            0, 0, 0, 0, hwnd, (HMENU)100, g_hinst, 0);
    
      return g_hwndStatus != NULL;
    }
    
    // add to WndProc
        case WM_MENUSELECT:
          MenuHelp(uiMsg, wParam, lParam, GetMenu(hwnd),
                   g_hinst, g_hwndStatus, rguiHelp);
          break;
    
    
    // change to InitApp
        wc.lpszMenuName = MAKEINTRESOURCE(1);
    

    Notice that this is identical to the code needed for the 16-bit MenuHelp function, except that we didn't initialize the UINT array with the pop-up menu handle.

    Run this program and see how the help text in the status bar changes based on which menu item you have seleced. The MenuHelp function also knows about the commands on the System menu and provides appropriate help text for those as well.

    Wow, this sounds like a neat function. Why then did I say that you probably will decide that you don't want to use it? Let's look at the limitations of the MenuHelp function.

    First, notice that all the help strings for the menu must come from the same HINSTANCE. Furthermore, the offset from the menu item identifier to the help string must remain constant across all menu items. These two points mean that you cannot build a menu out of pieces from multiple DLLs since you can pass only one HINSTANCE and offset.

    Second, the fixed offset means that you cannot have menus whose content expands dynamically, because you won't have help strings for the dynamic content. What's worse, if the dynamically-added menu item identifiers happen to, when added to the fixed offset, coincide with some other string resource, that other string resource will be used as the help string! For example, in our example above, if we dynamically added a menu item whose identifier is 1000, then the MenuHelp function would look for the string whose resource identifier is 1000 + 1000 = 2000. And if you happened to have some other string at position 2000 for some totally unrelated reason, that string will end up as the menu help.

    But hopefully you've spotted the fatal flaw in the MenuHelp function by now: That pop-up menu index. I carefully designed this example to avoid the flaw. The index of the "Text Size" pop-up menu is 2, and it is the only pop-up menu whose index is 2. (The "File" menu is index 0 and the "View" menu is index 1.) In real life, of course, you do not have the luxury of fiddling the menus to ensure that no two pop-up menus have the same index. And when they end up with the same index, the help strings get all confused since the MenuHelp function can't tell which of the multiple "second pop-up menu" you wanted to use string 2006 for.

    Could this be fixed? If you tried to return to the old HMENU-based way of identifying pop-ups, you'd run into some new problems: First, the introduction of 64-bit Windows means that you cannot just cast an HMENU to a UINT because an HMENU is a 64-bit value and UINT is only 32 bits. You could work around this by expanding the parameter to the MenuHelp function to be an array of UINT_PTR values instead of an array of UINTs, but that's not the only problem.

    The HMENU-base mechanism supports only one window at a time since the global array needs to be edited for each client. To make it support multiple windows, you would have to make a copy of the global array and edit the private copy. To avoid making a private copy, you would have to come up with some other way of specifying the pop-up window.

    Now, you could spend even more time trying to come up with a solution to the HMENU problem, but that still leaves the other problems we discussed earlier. Trying to salvage a MenuHelp-like solution to those problems leads to even more complicated mechanisms for expressing the relationship between a menu item identifier and the corresponding help string. Eventually, you come to the point where the general solution is too complicated for its own good and you're better off just coming up with an ad-hoc solution for your particular situation, like we did when we added menu help to our hosted shell context menus.

    (The only people I see using the MenuHelp function ignore dealing with pop-up menus and use only the first two UINTs, thereby avoiding the whole HMENU problem.)

  • The Old New Thing

    Disaster averted, thanks to undisclosed government action, no really

    • 16 Comments

    On his web site, http://www.savelivesinmay.com, Eric Julien predicted that (and I hope I got this right) on May 25, 2006, comet fragments generated in 1995 by a hostile extraterrestrial civilization would impact the Atlantic Ocean near the Azores, followed by volcanic eruptions which would create a giant tsunami that would wipe out the East coast of the United States.

    As the fateful day approached, the predicted date turned into a 48-hour window, and then triumphant relief that the United States government heeded his warning and prevented the tsunami from striking.

    But we're not out of the woods yet. "A catastrophe of an unprecedented size... may occur in the coming days if the media of the entire world does not inform the public immediately of the reality of the presence of extraterrestrials on Earth."

    They'll get right on that.

    (By the way, the Voyage to Our Hollow Earth, originally scheduled for June 26, 2005, then rescheduled for June 26, 2006 has now been rescheduled for June 26, 2007.)

  • The Old New Thing

    If you're going to try to simulate user actions, make sure the user can do them

    • 5 Comments

    Many people like to simulate user actions programmatically. I'm not going to comment here on whether that's a good idea to begin with; what I want to point out is that if you're going to do it, you have to make sure you're simulating things the user can actually do.

    For example, if you want to act as if the user clicked the "close" button in the corner, you have to make sure that the window is visible and enabled and that the window even has a "close" button to begin with! Failing to observe these rules means that the application you are manipulating is going to experience "impossible" conditions. The window may very well have been disabled in order to avoid the problem of the unsupported stack frame. Programmatically closing the window means that you've reintroduced the bug that the application programmer was trying to avoid.

    Similarly, if you want to simulate menu clicks, you need to check whether the menu item is enabled because a user can't select a disabled menu item. Since most programs do not synchronize menu item states until the menu is opened (since the only way a user can invoke a menu item is to open the enclosing menu first), you need to open the menu before you check whether the item is enabled.

    There are even more subtle conditions. For example, you might be dealing with a program that stops updating its status when the window is not visible, either because it is covered by another window or because the user has disconnected the session or locked the workstation. Under those conditions, the user can't interact with the program, and simulating such activity may result in the program behaving strangely because you're doing something "impossible".

    In the referenced article, the program wanted to set foreground activation to a particular window, but it didn't check whether the window was enabled or even visible! (And even if it were enabled and visible, it might be a WS_EX_NOACTIVATE window.)

    My goal here is not to come up with a comprehensive list of things to check before simulating every type of action you might want. That's something you need to figure out on your own: Look at what you're trying to simulate and think through what the window manager requires for that action to be accessible to the user.

  • The Old New Thing

    Apparently driving is messed up in a lot of countries

    • 25 Comments

    From the reactions to my entry on driving in Taiwan, it appears that driving is pretty messed up all over Asia. Here's a video of driving in India that was popular a while back.

    This comment comparing Taiwan driving to the Philippines reminded me of a conversation I had with some Filipino tourists when I was in Taiwan. We were on a bus as it was negotiating traffic, and I asked them, "Is driving in the Philippines as crazy as here in Taiwan?"

    They answered, "Oh, no. It's much crazier."

Page 3 of 4 (34 items) 1234