September, 2006

  • The Old New Thing

    IsBadXxxPtr should really be called CrashProgramRandomly

    • 81 Comments

    Often I'll see code that tries to "protect" against invalid pointer parameters. This is usually done by calling a function like IsBadWritePtr. But this is a bad idea. IsBadWritePtr should really be called CrashProgramRandomly.

    The documentation for the IsBadXxxPtr functions presents the technical reasons why, but I'm going to dig a little deeper. For one thing, if the "bad pointer" points into a guard page, then probing the memory will raise a guard page exception. The IsBadXxxPtr function will catch the exception and return "not a valid pointer". But guard page exceptions are raised only once. You just blew your one chance. When the code that is managing the guard page accesses the memory for what it thinks is the first time (but is really the second), it won't get the guard page exception but will instead get a normal access violation.

    Alternatively, it's possible that your function was called by some code that intentionally passed a pointer to a guard page (or a PAGE_NOACCESS page) and was expecting to receive that guard page exception or access violation exception so that it could dynamically generate the data that should go onto that page. (Simulation of large address spaces via pointer-swizzling is one scenario where this can happen.) Swallowing the exception in IsBadXxxPtr means that the caller's exception handler doesn't get a chance to run, which means that your code rejected a pointer that would actually have been okay, if only you had let the exception handler do its thing.

    "Yeah, but my code doesn't use guard pages or play games with PAGE_NOACCESS pages, so I don't care." Well, for one thing, just because your code doesn't use these features pages doesn't mean that no other code in your process uses them. One of the DLLs that you link to might use guard pages, and your use of IsBadXxxPtr to test a pointer into a guard page will break that other DLL.

    And second, your program does use guard pages; you just don't realize it. The dynamic growth of the stack is performed via guard pages: Just past the last valid page on the stack is a guard page. When the stack grows into the guard page, a guard page exception is raised, which the default exception handler handles by committing a new stack page and setting the next page to be a guard page.

    (I suspect this design was chosen in order to avoid having to commit the entire memory necessary for all thread stacks. Since the default thread stack size is a megabyte, this would have meant that a program with ten threads would commit ten megabytes of memory, even though each thread probably uses only 24KB of that commitment. When you have a small pagefile or are running without a pagefile entirely, you don't want to waste 97% of your commit limit on unused stack memory.)

    "But what should I do, then, if somebody passes me a bad pointer?"

    You should crash.

    No, really.

    In the Win32 programming model, exceptions are truly exceptional. As a general rule, you shouldn't try to catch them. And even if you decide you want to catch them, you need to be very careful that you catch exactly what you want and no more.

    Trying to intercept the invalid pointer and returning an error code creates nondeterministic behavior. Where do invalid pointers come from? Typically they are caused by programming errors. Using memory after freeing it, using uninitialized memory, that sort of thing. Consequently, an invalid pointer might actually point to valid memory, if for example the heap page that used to contain the memory has not been decomitted, or if the uninitialized memory contains a value that when reinterpreted as a pointer just happens to be a pointer to memory that is valid right now. On the other hand, it might point to truly invalid memory. If you use IsBadWritePtr to "validate" your pointers before writing to them, then in the case where it happens to point to memory that is valid, you end up corrupting memory (since the pointer is "valid" and you therefore decide to write to it). And in the case where it happens to point to an invalid address, you return an error code. In both cases, the program keeps on running, and then that memory corruption manifests itself as an "impossible" crash two hours later.

    In other words IsBadWritePtr is really CorruptMemoryIfPossible. It tries to corrupt memory, but if doing so raises an exception, it merely fails the operation.

    Many teams at Microsoft have rediscovered that IsBadXxxPtr causes bugs rather than fixes them. It's not fun getting a bucketful of crash dumps and finding that they are all of the "impossible" sort. You hunt through your code in search of this impossible bug. Maybe you find somebody who was using IsBadXxxPtr or equivalently an exception handler that swallows access violation exceptions and converts them to error codes. You remove the IsBadXxxPtr in order to let the exception escape unhandled and crash the program. Then you run the scenario again. And wow, look, the program crashes in that function, and when you debug it, you find the code that was, say, using a pointer after freeing it. That bug has been there for years, and it was manifesting itself as an "impossible" bug because the function was trying to be helpful by "validating" its pointers, when in fact what it was doing was taking a straightforward problem and turning it into an "impossible" bug.

    There is a subtlety to this advice that you should just crash when given invalid input, which I'll take up next time.

  • The Old New Thing

    Allocating and freeing memory across module boundaries

    • 55 Comments

    I'm sure it's been drilled into your head by now that you have to free memory with the same allocator that allocated it. LocalAlloc matches LocalFree, GlobalAlloc matches GlobalFree, new[] matches delete[]. But this rule goes deeper.

    If you have a function that allocates and returns some data, the caller must know how to free that memory. You have a variety of ways of accomplishing this. One is to state explicitly how the memory should be freed. For example, the FormatMessage documentation explicitly states that you should use the LocalFree function to free the buffer that is allocated if you pass the FORMAT_MESSAGE_ALLOCATE_BUFFER flag. All BSTRs must be freed with SysFreeString. And all memory returned across COM interface boundaries must be allocated and freed with the COM task allocator.

    Note, however, that if you decide that a block of memory should be freed with the C runtime, such as with free, or with the C++ runtime via delete or delete[], you have a new problem: Which runtime?

    If you choose to link with the static runtime library, then your module has its own private copy of the C/C++ runtime. When your module calls new or malloc, the memory can only be freed by your module calling delete or free. If another module calls delete or free, that will use the C/C++ runtime of that other module which is not the same as yours. Indeed, even if you choose to link with the DLL version of the C/C++ runtime library, you still have to agree which version of the C/C++ runtime to use. If your DLL uses MSVCRT20.DLL to allocate memory, then anybody who wants to free that memory must also use MSVCRT20.DLL.

    If you're paying close attention, you might spot a looming problem. Requiring all your clients to use a particular version of the C/C++ runtime might seem reasonable if you control all of the clients and are willing to recompile all of them each time the compiler changes. But in real life, people often don't want to take that risk. "If it ain't broke, don't fix it." Switching to a new compiler risks exposing a subtle bug, say, forgetting to declare a variable as volatile or inadvertently relying on temporaries having a particular lifetime.

    In practice, you may wish to convert only part of your program to a new compiler while leaving old modules alone. (For example, you may want to take advantage of new language features such as templates, which are available only in the new compiler.) But if you do that, then you lose the ability to free memory that was allocated by the old DLL, since that DLL expects you to use MSVCRT20.DLL, whereas the new compiler uses MSVCR71.DLL.

    The solution to this requires planning ahead. One option is to use a fixed external allocator such as LocalAlloc or CoTaskMemAlloc. These are allocators that are universally available and don't depend on which version of the compiler you're using.

    Another option is to wrap your preferred allocator inside exported functions that manage the allocation. This is the mechanism used by the NetApi family of functions. For example, the NetGroupEnum function allocates memory and returns it through the bufptr parameter. When the caller is finished with the memory, it frees it with the NetApiBufferFree function. In this manner, the memory allocation method is isolated from the caller. Internally, the NetApi functions might be using LocalAlloc or HeapAllocate or possibly even new and free. It doesn't matter; as long as NetApiBufferFree frees the memory with the same allocator that NetGroupEnum used to allocate the memory in the first place.

    Although I personally prefer using a fixed external allocator, many people find it more convenient to use the wrapper technique. That way, they can use their favorite allocator throughout their module. Either way works. The point is that when memory leaves your DLL, the code you gave the memory to must know how to free it, even if it's using a different compiler from the one that was used to build your DLL.

  • The Old New Thing

    Why doesn't the Shutdown dialog use Alt to get alternate behavior?

    • 59 Comments

    When you select "Shut Down" from the Start menu, a dialog appears with three options: "Stand By", "Turn Off" and "Restart". To get the secret fourth option "Hibernate" you have to press the shift key. Would the Alt key be the more obvious choice for revealing alternate options?

    You might think so, but it so happens that Alt already has meaning. In this dialog, the Alt key would be a disaster, because the underlined letters indicate keyboard accelerators, which are pressed in conjunction with the Alt key. In other words, from the Shut Down dialog, you can type Alt+S to stand by, Alt+U to turn off, or Alt+R to restart. Since the Alt key was already taken, the Shift key had to be used to reveal the bonus options. Using the Shift key to reveal bonus options is not uncommon. You can hold the Shift key while right-clicking on a file to get an extended context menu, and of course there's the Shift+No option in file copy dialogs to mean "No to all".

    In fact, you don't need to press the Alt or Shift keys at all. Recall that the rules for dialog box navigation permit omitting the Alt key if the focus control does not accept character input; since the only things on the Shut Down dialog are pushbuttons, there is no character input and you can just press "S", "U" or "R" without the Alt key. What's more, you don't need to hold the Shift key if you want to shut down; you can just type "H" and the Hibernate option will be invoked, because hotkeys for hidden controls are still active.

  • The Old New Thing

    Raymond's excursions into East Asian pop music, episode 3: Morning Musume (モーニング娘)

    • 26 Comments

    It really all started with Katamari Damacy (塊魂). The music for that game is so darned infectious, and it was my fascination with that music that prompted my colleague to loan me the CDs his wife bought while she traveled through Asia. I already mentioned China Dolls (中國娃娃). Another of the CDs in the collection was 4th Ikimasshoi! (4th いきまっしょい! = 4th Let's Go!), the um fourth album from the J-Pop group Morning Musume (モーニング娘 = Morning Girls). I'm sure somebody will correct my Japanese translation.

    Yes, these are the girls who in the United States are probably known only for having pork chops tied to their foreheads while being stalked by a lizard or being chased by American fighter Bob Sapp or being freaked out by a clip from the movie The Ring or traumatizing one of its members by dressing her up like a seal and making her hang out at the polar bear tank. From what I can gather, they aren't so much a pop music group as a marketing phenomenon, what with their own television show and endorsement contracts. And yes, it's a singing group with thirteen members. Thirteen. When I first glanced at the album cover, I just assumed that it was the same four or five singers dressed up in different costumes, but no, it really is a group with a ridiculous number of members.

    Their music is bubble-gum J-Pop, often catchy, but sometimes just plain awful. (And sometimes really awful or horrifically I-can't-even-watch-it awful.) But I found at least the catchy tunes useful, because they're energetic and kept me going on longer bicycle rides. It probably helped that I didn't understand the words, though I strongly suspect they're singing about love. (I also find that even the catchy songs tend to be ruined by the videos.)

    Setting aside the musical merits, I have to admire the logistics of organizing a performance of such a large group. Compare, for example, this music video for Osaka Koi no Uta (大阪 恋の歌 = Osaka Love Song) with a live performance of same. In the music video, you can just cut from one vocalist to the next, but in the live performance, the singers have to physically trade places. It's so complicated that some dedicated fans have color-coded the lyrics to keep track of who sings what.

    Another of my colleagues more tuned into the contemporary music scene learned of my fascination with Japanese pop music and dedicated himself to finding some good Japanese pop music, just to show me that it's not all bubble-gum. More on that in the next episode.

  • The Old New Thing

    Waiting until the dialog box is displayed before doing something

    • 30 Comments

    Last time, I left you with a few questions. Part of the answer to the first question was given in the comments, so I'll just link to that. The problem is more than just typeahead, though. The dialog box doesn't show itself until all message traffic has gone idle. If you actually ran the code presented in the original message, you'd find that it didn't actually work!

    #include <windows.h>
    
    INT_PTR CALLBACK
    DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
    {
      switch (uiMsg) {
    
      case WM_INITDIALOG:
        PostMessage(hwnd, WM_APP, 0, 0);
        return TRUE;
    
      case WM_APP:
        MessageBox(hwnd,
                  IsWindowVisible(hwnd) ? TEXT("Visible")
                                        : TEXT("Not Visible"),
                   TEXT("Title"), MB_OK);
        break;
    
      case WM_CLOSE:
       EndDialog(hwnd, 0);
       break;
      }
    
      return FALSE;
    }
    
    int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                       LPSTR lpCmdLine, int nShowCmd)
    {
        DialogBox(hinst, MAKEINTRESOURCE(1), NULL, DlgProc);
        return 0;
    }
    

    When you run this program, the message box says "Not Visible", and in fact when it appears, you can see that the main dialog is not yet visible. It doesn't show up until after you dismiss the message box.

    Mission: Not accomplished.

    Along the way, there was some dispute over whether the private message should be WM_USER or WM_APP. As we saw before, window messages in the WM_USER range belong to the window class, and in this case, the window class is the dialog window class, i.e., WC_DIALOG. Since you are not the implementor of the dialog window class (you didn't write the window procedure), the WM_USER messages are not yours for the taking. And in fact, if you had decided to use WM_USER you would have run into all sorts of problems, because it so happens that the dialog manager already defined that message for its own purposes:

    #define DM_GETDEFID         (WM_USER+0)
    

    When the dialog manager sends the dialog a DM_GETDEFID message to obtain the default control ID, you will think it's your WM_USER message and show your dialog box. It turns out that the dialog manager uses the default control ID rather often, and as a result, you're going to display an awful lot of dialog boxes. (Even worse, your second dialog box will probably use the dialog itself as the owner, which then leads to the problem of having a dialog box with multiple modal children, which then leads to disaster when they are dismissed by the user in the wrong order.)

    Okay, so we're agreed that we should use WM_APP as the private message.

    Some people suggested using a timer, on the theory that timer messages are lower priority than paint messages, so the timer won't fire until all painting is done. While that's true, it also doesn't help. The relative priority of timer and paint messages comes into play only if the window manager has to choose between timers and paint messages when deciding which one to deliver first. But if there are no paint messages needed in the first place, then timers are free to go ahead.

    And when the window is not visible, it doesn't need any paint messages. In a sense, the timer approach misses the point entirely: It's trying to take advantage of paint messages being higher priority precisely in the scenario where there are no paint messages!

    Let's demonstrate this by implementing the timer approach, but I'm going to add a twist to make the race condition clearer:

    ...
    
    INT_PTR CALLBACK
    DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
    {
      switch (uiMsg) {
    
      case WM_INITDIALOG:
        SetTimer(hwnd, 1, 1, 0);
        Sleep(100); //simulate paging
        return TRUE;
    
      case WM_TIMER:
        if (wParam == 1) {
          KillTimer(hwnd, 1);
          MessageBox(hwnd,
                    IsWindowVisible(hwnd) ? TEXT("Visible")
                                          : TEXT("Not Visible"),
                     TEXT("Title"), MB_OK);
        }
        break;
    
      case WM_CLOSE:
       EndDialog(hwnd, 0);
       break;
      }
    
      return FALSE;
    }
    

    If you run this program, you'll see the message "Not Visible". I inserted an artificial Sleep(100) to simulate the case where the code takes a page fault and has to wait 100ms for the code to arrive from the backing store. (Maybe it's coming from the network or a CD-ROM, or maybe the local hard drive is swamped with I/O and you have to wait that long for your paging request to become satisfied after all the other I/O requests active on the drive.)

    As a result of that Sleep(), the dialog manager doesn't get a chance to empty the message queue and show the window because the timer message is already in the queue. Result: The timer fires and the dialog is still hidden.

    Some people waited for WM_ACTIVATE, but that tells you when the window becomes active, which is not the same as being shown, so it doesn't satisfy the original requirements.

    Others suggested waiting for WM_PAINT, but a window can be visible without painting. The WM_PAINT message arrives if the window's client area is uncovered, but the caption bar might still be visible even if the client area is covered. Furthermore, while this addresses the problem if you interpret "visible" as "results in pixels on the screen", as opposed to IsWindowVisible, you need to look behind the actual request to what the person was really looking for. (This is an important skill to have because people rarely ask for what they want, but rather for what they think they want.) The goal was to create a dialog box and have it look like the user automatically clicked a button on it to call up a secondary dialog. In order to get this look, the base dialog needs to be visible before the secondary dialog can be displayed.

    One approach is to show the second dialog on receipt of the WM_SHOWWINDOW, but even that is too soon:

    // In real life, this would be an instance variable
    BOOL g_fShown = FALSE;
    
    INT_PTR CALLBACK
    DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
    {
      switch (uiMsg) {
    
      case WM_INITDIALOG:
        return TRUE;
    
      case WM_SHOWWINDOW:
        if (wParam && !g_fShown) {
          g_fShown = TRUE;
          MessageBox(hwnd,
                     IsWindowVisible(hwnd) ? TEXT("Visible")
                                           : TEXT("Not Visible"),
                     TEXT("Title"), MB_OK);
        }
        break;
    
      case WM_CLOSE:
       EndDialog(hwnd, 0);
       break;
      }
    
      return FALSE;
    }
    

    (Subtlety: Why do I set g_fShown = TRUE before displaying the message box?)

    If you run this program, you will still get the message "Not Visible" because WM_SHOWWINDOW is sent as part of the entire window-showing process. At the time you receive it, your window is in the process of being show but it's not quite there yet. The WM_SHOWWINDOW serves a similar purpose to WM_INITDIALOG: To let you prepare the window while it's still hidden so the user won't see ugly flashing which would otherwise occur if you had done your preparation after the window were visible.

    Is there a message that is sent after the window has been shown? There sure is: WM_WINDOWPOSCHANGED.

    INT_PTR CALLBACK
    DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
    {
      switch (uiMsg) {
    
      case WM_INITDIALOG:
        return TRUE;
    
      case WM_WINDOWPOSCHANGED:
        if ((((WINDOWPOS*)lParam)->flags & SWP_SHOWWINDOW) &&
            !g_fShown) {
          g_fShown = TRUE;
          MessageBox(hwnd,
                     IsWindowVisible(hwnd) ? TEXT("Visible")
                                           : TEXT("Not Visible"),
                     TEXT("Title"), MB_OK);
        }
        break;
    
      case WM_CLOSE:
       EndDialog(hwnd, 0);
       break;
      }
    
      return FALSE;
    }
    

    This time, we get the message "Visible", because WM_WINDOWPOSCHANGED is sent after the window positioning negotiations are complete. (The "ED" at the end emphasizes that it is delivered after the operation has been done, as opposed to the "ING" which is delivered while the operation is in progress.)

    But wait, we're not out of the woods yet. Although it's true that the window position negotiations are complete, the message is nevertheless sent as part of the whole window positioning process, and there may be other things that need to be done as part of the whole window-showing bookkeeping. If you show the second dialog directly in your WM_WINDOWPOSCHANGED handler, then that clean-up won't happen until after the user exits the second dialog.

    For example, the window manager notifies Active Accessibility of the completed window positioning operation after all the window positions have settled down. This reduces the likelihood that the accessibility tool will be told "Okay, the window is shown" followed by "Oh no wait, it moved again, ha ha!" If you display the second dialog inside your WM_WINDOWPOSCHANGED handler, the screen reader will receive a bizarro sequence of events:

    • Second dialog shown.
    • (User interacts with second dialog and dismisses it.)
    • Second dialog destroyed.
    • (Your WM_WINDOWPOSCHANGED handler returns.)
    • Main dialog shown.

    Notice that the "Main dialog shown" notification arrives out of order because you did additional UI work before the previous operation was complete.

    As another example, the window may have been shown as part of a multiple-window window positioning operation such as one created by DeferWindowPos. All the affected windows will get their WM_WINDOWPOSCHANGED notifications one at a time, and if your window happened to go first, then those other windows won't know that they were repositioned until after the user finishes with the nested dialog. This may manifest itself in those other windows appearing to be "stuck" since your dialog is holding up the subsequent notifications with your nested dialog. For example, a window might be trying to do exactly what you're trying to do here, but since you're holding up the remainder of the notifications, that other window won't display its secondary dialog until the user dismisses yours. From the user's standpoint, that other window is "stuck" for no apparent reason.

    Therefore, we need one more tweak to our solution.

    INT_PTR CALLBACK
    DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
    {
      switch (uiMsg) {
    
      case WM_INITDIALOG:
        return TRUE;
    
      case WM_WINDOWPOSCHANGED:
        if ((((WINDOWPOS*)lParam)->flags & SWP_SHOWWINDOW) &&
            !g_fShown) {
          g_fShown = TRUE;
          PostMessage(hwnd, WM_APP, 0, 0);
        }
        break;
    
    
      case WM_APP:
          MessageBox(hwnd,
                     IsWindowVisible(hwnd) ? TEXT("Visible")
                                           : TEXT("Not Visible"),
                     TEXT("Title"), MB_OK);
          break;
    
      case WM_CLOSE:
       EndDialog(hwnd, 0);
       break;
      }
    
      return FALSE;
    }
    

    When we learn that the dialog is being shown for the first time, we post a message to ourselves to display the secondary dialog and return from the WM_WINDOWPOSCHANGED handler. This allows the window positioning operation to complete. Everybody gets their notifications, they are all on board with the state of the windows, and only after everything has stabilized do we display our message box.

    This is a common thread to many types of window management. Many window messages are notifications which are delivered while the operation is still in progress. You do not want to display new UI while handling those notifications because that holds up the completion of the original UI operation that generated the notification in the first place. Posting a message to yourself to complete the user interaction after the original operation has stabilized is the standard solution.

  • The Old New Thing

    Grammar review: Verb+particle versus compound noun

    • 32 Comments

    Although the inflections and compound-mania are largely absent from the English language, there are still some vestiges of its Germanic roots. One detail of English grammar that I often see neglected is the distinction between the verb+particle and the compound noun.

    Consider the verb phrase "to shut down", which is the one I see misused most often. This is a verb+particle combination and is treated as two words. When you turn it into a noun, however, it becomes "shutdown", one word. This Knowledge Base article, for example, manages to keep its head on straight for most of the article, using the verb+particle for the verb form and the compound for the noun form:

    \\computername: Use this switch to specify the remote computer to shut down.

    /a: Use this switch to quit a shutdown operation.

    But then it slips up towards the end and uses the compound as a verb:

    To schedule the local computer to shutdown and restart at 10:00 P.M. ...

    In other Germanic languages the distinction is clearer. Consider the Swedish and German verbs for "to make up" (as in, "to make up an alibi"):

    hitta på   påhittad
    legen zurecht   zurechtlegen

    In the verb+particle form, the particle comes after the verb, whereas in the single-word form, the particle comes before the verb. It's therefore more obvious when you have one word and when you have two. English does this only rarely, typically for verbs that retain poetic or archaic appeal ("cast down" → "downcast") and therefore reach back to the language's German roots for their power.

    This is one of the reasons why I'm so fascinated by the Germanic languages: The more I learn about the other languages, the more I learn about my own.

  • The Old New Thing

    Quotation marks around spaces aren't necessary in the PATH environment variable

    • 28 Comments

    The purpose of quotation marks is to allow a character that would normally be interpreted as a delimiter to be included as part of a file name. Most of the time, this delimiter is the space. The CreateProcess function uses a space to separate the program name from its arguments. Most programs separate their command line arguments with a space. But the PATH environment variable doesn't use spaces to separate directories. It uses semicolons.

    This means that if you want to add a directory with spaces in its name to the path, you don't need quotation marks since spaces mean nothing to the PATH environment variable. The quotation marks don't hurt, mind you, but they don't help either.

    On the other hand, if the directory you want to add contains a semicolon in its name, then you do need the quotation marks, because it's the semicolon that you need to protect.

  • The Old New Thing

    If you ask a Yes/No question, make sure the user also knows what happens when they say No

    • 63 Comments

    I was talking with someone last year who had a gripe about a music organizer program. Suppose you create some playlists and then decide, "Oh, nevermind, I don't like this playlist." You highlight the playlist and click "Delete". You then get a dialog box that asks, "Do you want to move the songs in this playlist to the Recycle Bin?"

    "Well, no, I don't want you to recycle those songs. I want to keep the songs. Just delete the playlist," you say to yourself and you click "No".

    Unfortunately, the program was asking you, "Do you want to move the songs to the Recycle Bin or delete the songs permanently from your computer?" The program had already decided that you wanted to delete the songs themselves when you deleted the playlist. It just wanted to know whether you wanted them gone immediately or just tossed into the Recycle Bin. Fortunately, my friend had backups of the songs that had mistakenly been purged from the computer, but it was still quite shocking to see all the music just plain disappear when there was no expectation that anything of the sort was going to happen.

    When programs put up Yes/No dialogs, they usually don't have a problem explaining what will happen when you click Yes. But they also have to make sure users understand what will happen when they click No.

    Window Vista's new Task Dialog makes it easier for programs to make it clearer to users what will happen as the result of pushing a button on a dialog box. Instead of being limited to just "Yes" and "No", you can put more meaningful text on the buttons such as "Save" and "Don't Save", or you could use command buttons and provide an explanatory sentence for each option. Now, programs could always have built custom dialogs with these more descriptive buttons, but doing so meant designing a dialog box from scratch, positioning the buttons precisely according to dialog box layout guidelines, and then writing a custom dialog procedure to handle this new custom dialog. Most people just take the easy way out and use MessageBox. In Windows Vista there is now a way to build slightly more complex dialogs without having to design a dialog template.

    Be careful, however, not to fall into the same trap with task dialogs. The original dialog might have been converted to a task dialog with the buttons "Recycle" and "Don't Recycle", which would not have solved the problem at all.

  • The Old New Thing

    On the unanswerability of the maximum number of user interface objects a program can create

    • 23 Comments

    The answer to the question "What is the maximum number of window classes a program can register?" is not a number.

    Most user interface objects come from a shared pool of memory known as the "desktop heap". Although one could come up with a theoretical maximum number of window classes that can fit in the desktop heap, that number is not achievable in practice because the desktop heap is shared with all other user interface objects on the desktop. For example, menus and windows go into the desktop heap, as well as more obscure objects like active window enumerations, window positioning handles used by DeferWindowPos, and even how many threads have attached input queues (either implicitly done by having cross-thread parent/owner relationships or explicitly by calling the AttachThreadInput function). The more windows and menus you have, the less space available for other things like registered window classes.

    Typically, when somebody asks this question, the real problem is that they designed a system to the point where desktop heap exhaustion has become an issue, and they need to redesign the program so they aren't so wasteful of desktop heap resources in general. (One customer, for example, was registering thousands of window classes in their program, which is excessive.) In the same way that somebody asking for the maximum number of threads a process can create is an indication that their program is in need of a redesign, a program that registers thousands of window classes needs to be given another look. After all, even just creating a thousand windows is excessive—any UI that shows the user a thousand windows is too confusing to be usable.

    (Pre-emptive link: Q126962: On the desktop heap.)

  • The Old New Thing

    It's that season again: The Microsoft Company Meeting

    • 16 Comments

    Today is the 2006 Microsoft Company Meeting, and with it the continuation of what I consider to be one of the most annoying Company Meeting traditions: The group that cheers wildly any time their project name is mentioned.

    It's never the same group year to year. Instead, a different group (or groups) independent decides to be the annoying one for any particular meeting. For illustrative purposes, let's call the 2006 group "Project Nosebleed". (All project names in this entry are fictitious and are used solely for illustrative purposes. Any similarity to actual projects is purely coincidental. But I mean, really, you asked for it when you named your project after a medical ailment.) The Project Nosebleed folks decide to cheer wildly any time anybody says the word "Nosebleed" at the Company Meeting. Let's see what happens when one presenter mentions their project name:

    We have a lot of products that are poised to make a splash
    in the upcoming year. For the home user, we have Project
    Vertigo, which will help them organize their time and schedule.
    On the small-business side, Project Nosebleed will bring the
    ease-of-use that people have come to expect from their PC Woohoo! Yippee! Rah! Rah! Rah! NoseBleed Number One!  
    to the unwilling IT guy. Meanwhile, Bunion will do the same Nose-Bleed! Nose-Bleed! Yay! Yay!  
    for the educational market. So you see, there are a lot of
    exciting things waiting in the wings.

    Thank you, Nosebleed team. You shouted down the presenter telling the rest of the company what your project is, as well as drowning out the introduction of another team's project. All the rest of the company will remember about your project is "Those annoying Nosebleeders kept screaming so I couldn't hear what the presenter was saying." Well, not all the rest of the company. The Bunion folks will remember a little more. "Those annoying Nosebleeders kept screaming so nobody could hear what our project was about!"

    I remember one year, the team that cheered wildly at every mention of their project did it a bit too mindlessly. A pre-recorded interview was being screened, and the interviewee mentioned their project in a negative way, but they cheered anyway.

    Way to go, Project Nosebleed.

Page 1 of 4 (37 items) 1234