• The Old New Thing

    Double-clicking radio buttons

    • 9 Comments

    A subtlety that adds a level of polish to your dialogs is supporting double-clicked radio buttons as an abbreviation for "select + OK". (Or "select + Next" or "select + Finish" if the page is part of a wizard.)

    Consider the following dialog template and associated dialog procedure:

    1 DIALOGEX DISCARDABLE  32, 32, 200, 76
    STYLE DS_MODALFRAME |  WS_POPUP |
          WS_VISIBLE | WS_CAPTION | WS_SYSMENU
    CAPTION "Sample"
    FONT 8, "MS Shell Dlg"
    BEGIN
     LTEXT "A mumbler is needed.",-1,7,8,100,10
     AUTORADIOBUTTON "Do not &obtain a mumber now",
                     100,17,24,180,10
     AUTORADIOBUTTON "Obtain a mumbler auto&matically",
                     101,17,34,180,10
     AUTORADIOBUTTON "&Enter mumbler manually",
                     102,17,44,180,10
     DEFPUSHBUTTON "OK",IDOK,92,58,50,14
     PUSHBUTTON "Cancel",IDCANCEL,146,58,50,14
    END
    
    INT_PTR CALLBACK DlgProc(HWND hdlg, UINT uMsg,
                             WPARAM wParam, LPARAM lParam)
    {
     switch (uMsg) {
     case WM_INITDIALOG:
      CheckRadioButton(hdlg, 100, 102, 100);
      return TRUE;
     case WM_COMMAND:
      switch (GET_WM_COMMAND_ID(wParam, lParam)) {
      case IDOK:
       for (int i = 100; i <= 102; i++) {
        if (IsDlgButtonChecked(hdlg, i)) EndDialog(hdlg, i);
       }
       break;
      case IDCANCEL:
       EndDialog(hdlg, -1);
       break;
      }
     }
     return FALSE;
    }
    

    This is pretty standard unexciting dialog box that asks the user to select an option from a list. Notice that double-clicking the radio button doesn't do anything special. We can fix that.

    1 DIALOGEX DISCARDABLE  32, 32, 200, 76
    STYLE DS_MODALFRAME |  WS_POPUP |
          WS_VISIBLE | WS_CAPTION | WS_SYSMENU
    CAPTION "Sample"
    FONT 8, "MS Shell Dlg"
    BEGIN
     LTEXT "A mumbler is needed.",-1,7,8,100,10
     AUTORADIOBUTTON "Do not &obtain a mumber now",
                     100,17,24,180,10,BS_NOTIFY
     AUTORADIOBUTTON "Obtain a mumbler auto&matically",
                     101,17,34,180,10,BS_NOTIFY
     AUTORADIOBUTTON "&Enter mumbler manually",
                     102,17,44,180,10,BS_NOTIFY
     DEFPUSHBUTTON "OK",IDOK,92,58,50,14
     PUSHBUTTON "Cancel",IDCANCEL,146,58,50,14
    END
    
    INT_PTR CALLBACK DlgProc(HWND hdlg, UINT uMsg,
                             WPARAM wParam, LPARAM lParam)
    {
     switch (uMsg) {
     case WM_INITDIALOG:
      CheckRadioButton(hdlg, 100, 102, 100);
      return TRUE;
     case WM_COMMAND:
      switch (GET_WM_COMMAND_ID(wParam, lParam)) {
      case IDOK:
       for (int i = 100; i <= 102; i++) {
        if (IsDlgButtonChecked(hdlg, i)) EndDialog(hdlg, i);
       }
       break;
      case IDCANCEL:
       EndDialog(hdlg, -1);
       break;
      case 100:
      case 101:
      case 102:
       if (GET_WM_COMMAND_CMD(wParam, lParam) == BN_DBLCLK) {
        EndDialog(hdlg, GET_WM_COMMAND_ID(wParam, lParam));
       }
       break;
      }
     }
     return FALSE;
    }
    

    We added the BS_NOTIFY style to the radio buttons, which enables the extended notifications (everything other than BN_CLICKED). When we receive a WM_COMMAND message for a radio button specifying that the operation was BN_DBLCLK, we automatically click the OK button. (For a wizard, we would automatically click the Next or Finish button, as appropriate.)

    Note that double-click as a shortcut for select-and-OK should be used only for dialogs or wizard pages where the only relevant controls are radio buttons (and the OK and Cancel buttons). If there are controls on the page other than the radio button, then you have to wait for the OK because the user might want to manipulate those other controls too.

    This "double-click means select-and-OK" shortcut can also be used if the only thing on the dialog is a list box or list view from with the user is being asked to select one item. If the user double-clicks an item from the list, treat it as select-and-OK.

    The dialog boxes in Explorer are rather inconsistent in their support for double-clicked radio buttons. The Office and Money teams do a much better job.

  • The Old New Thing

    Watch out, there are Swedes in the hallway!

    • 10 Comments

    Whenever I go to IKEA Seattle, I try to speak Swedish with the people who work there, but it never works.

    Yesterday, I was heading to a colleague's office to drop off Symphony tickets. (A group of friends wanted tickets together, so the order was placed in my name, and I needed to distribute them to the people in the group.) As I neared his office, I noticed a crowd of people outside and realized that they were speaking Swedish. I tried to follow what they were saying, but of course since they were speaking among themselves, they weren't taking very much effort to enunciate clearly enough for a beginner to follow.

    The solution, therefore, is to change the subject.

    I said hello and told them I had been studying Swedish for two years. One of them asked me a question that I couldn't understand, so I apologized for my bad Swedish, but they insisted it was "jättebra" (great). How they could conclude that from one misunderstood question I'm not sure. They asked me simpler questions like whether I had ever lived in Sweden (no), and why I started studying it in the first place.

    After a brief chat, I excused myself to deliver the tickets. Afterwards, I realized that I forgot to ask them whether they were Microsoft employees or whether they were visiting (and if visiting, from what company).

  • The Old New Thing

    Drawing a monochrome bitmap with transparency

    • 6 Comments
    Last time, I left you with a brief puzzle. Here are two approaches. I am not a GDI expert, so there may be even better solutions out there. To emphasize the transparency, I'll change the window background color to the application workspace color.

     BOOL WinRegisterClass(WNDCLASS *pwc)
     {
      pwc->hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1);
      return __super::WinRegisterClass(pwc);
     }
    

    Method 1: A big MaskBlt.

    void RootWindow::PaintContent(PAINTSTRUCT *pps)
    {
     HDC hdcMem = CreateCompatibleDC(pps->hdc);
     if (hdcMem) {
      int cxCheck = GetSystemMetrics(SM_CXMENUCHECK);
      int cyCheck = GetSystemMetrics(SM_CYMENUCHECK);
      HBITMAP hbmMono = CreateBitmap(cxCheck, cyCheck, 1, 1, NULL);
      if (hbmMono) {
       HBITMAP hbmPrev = SelectBitmap(hdcMem, hbmMono);
       if (hbmPrev) {
        RECT rc = { 0, 0, cxCheck, cyCheck };
        DrawFrameControl(hdcMem, &rc, DFC_MENU, DFCS_MENUCHECK);
        COLORREF clrTextPrev = SetTextColor(pps->hdc,
                                         GetSysColor(COLOR_MENUTEXT));
        // COLORREF clrBkPrev = SetBkColor(pps->hdc,
        //                                  GetSysColor(COLOR_MENU));
        MaskBlt(pps->hdc, 0, 0, cxCheck, cyCheck,
               hdcMem, 0, 0, hbmMono, 0, 0
               MAKEROP4(0x00AA0029, SRCCOPY));
        // SetBkColor(pps->hdc, clrBkPrev);
        SetTextColor(pps->hdc, clrTextPrev);
        SelectBitmap(hdcMem, hbmPrev);
       }
       DeleteObject(hbmMono);
      }
      DeleteDC(hdcMem);
     }
    }
    

    This has the least amount of typing but feels like overkill to me, using a quaternary raster operation as if were a ternary, just because I didn't want to create a pattern brush. (The raster operation 0x00AA0029 is the NOP operator; it leaves the destination alone. I didn't have this memorized; I looked it up in the documentation.) The MAKEROP4 says that for each white pixel in the mask, do nothing (NOP), and for each black pixel, do a SRCCOPY.

    Notice that the background color is never used (since it's supposed to be transparent); consequently, we can delete the code that sets and restores the DC's background color.

    Method 2: The traditional two-step.

    void RootWindow::PaintContent(PAINTSTRUCT *pps)
    {
     HDC hdcMem = CreateCompatibleDC(pps->hdc);
     if (hdcMem) {
      int cxCheck = GetSystemMetrics(SM_CXMENUCHECK);
      int cyCheck = GetSystemMetrics(SM_CYMENUCHECK);
      HBITMAP hbmMono = CreateBitmap(cxCheck, cyCheck, 1, 1, NULL);
      if (hbmMono) {
       HBITMAP hbmPrev = SelectBitmap(hdcMem, hbmMono);
       if (hbmPrev) {
        RECT rc = { 0, 0, cxCheck, cyCheck };
        DrawFrameControl(hdcMem, &rc, DFC_MENU, DFCS_MENUCHECK);
        COLORREF clrTextPrev = SetTextColor(pps->hdc, RGB(0,0,0));
        COLORREF clrBkPrev = SetBkColor(pps->hdc, RGB(255,255,255));
        BitBlt(pps->hdc, cxCheck, 0, cxCheck, cyCheck,
               hdcMem, 0, 0, SRCAND);
        SetTextColor(pps->hdc, GetSysColor(COLOR_MENUTEXT));
        SetBkColor(pps->hdc, RGB(0,0,0));
        BitBlt(pps->hdc, cxCheck, 0, cxCheck, cyCheck,
               hdcMem, 0, 0, SRCPAINT);
        SetBkColor(pps->hdc, clrBkPrev);
        SetTextColor(pps->hdc, clrTextPrev);
        SelectBitmap(hdcMem, hbmPrev);
       }
       DeleteObject(hbmMono);
      }
      DeleteDC(hdcMem);
     }
    }
    

    This is the traditional two-step blit. The first erases the pixels that are about to be overwritten by setting the foreground to black and background to white, then using SRCAND. This has the effect of erasing all the foreground pixels to zero while leaving the background intact. The second blit does the same, but with SRCPAINT. This means that the background pixels need to be treated as black, so that when they are "or"d with the destination, the destination pixels are unchanged. The foreground pixels get the desired foreground color.

    This method can be shortened by negating the first blit, reversing the sense of foreground and background, so that the color black doesn't have to move between the background color and the text color.

    void RootWindow::PaintContent(PAINTSTRUCT *pps)
    {
     HDC hdcMem = CreateCompatibleDC(pps->hdc);
     if (hdcMem) {
      int cxCheck = GetSystemMetrics(SM_CXMENUCHECK);
      int cyCheck = GetSystemMetrics(SM_CYMENUCHECK);
      HBITMAP hbmMono = CreateBitmap(cxCheck, cyCheck, 1, 1, NULL);
      if (hbmMono) {
       HBITMAP hbmPrev = SelectBitmap(hdcMem, hbmMono);
       if (hbmPrev) {
        RECT rc = { 0, 0, cxCheck, cyCheck };
        DrawFrameControl(hdcMem, &rc, DFC_MENU, DFCS_MENUCHECK);
        COLORREF clrTextPrev = SetTextColor(pps->hdc, RGB(255,255,255));
        COLORREF clrBkPrev = SetBkColor(pps->hdc, RGB(0,0,0));
        BitBlt(pps->hdc, cxCheck, 0, cxCheck, cyCheck,
               hdcMem, 0, 0, 0x00220326); // DSna
        SetTextColor(pps->hdc, GetSysColor(COLOR_MENUTEXT));
        BitBlt(pps->hdc, cxCheck, 0, cxCheck, cyCheck,
               hdcMem, 0, 0, SRCPAINT);
        SetBkColor(pps->hdc, clrBkPrev);
        SetTextColor(pps->hdc, clrTextPrev);
        SelectBitmap(hdcMem, hbmPrev);
       }
       DeleteObject(hbmMono);
      }
      DeleteDC(hdcMem);
     }
    }
    

    Whether this shortening is actually an overall improvement is difficult to tell. It's possible that some display drivers have a highly optimized SRCAND handler whereas they are less likely to have an optimized 0x00220326 handler.

    (Exercise: Why can't you instead reverse the second blit, converting it to a MERGEPAINT?)

  • The Old New Thing

    Rendering menu glyphs is slightly trickier

    • 11 Comments

    Last time, we saw how to draw themed and unthemed radio buttons, and I mentioned that menu glyphs are trickier. They're trickier because they are provided as raw monochrome bitmaps instead of fully-formed color-coordinated bitmaps. First, let's do it wrong in order to see what we get. Then we'll try to fix it. Start with a clean new scratch program

    class RootWindow : public Window
    {
     ...
    protected:
     void PaintContent(PAINTSTRUCT *pps);
     BOOL WinRegisterClass(WNDCLASS *pwc)
     {
      pwc->hbrBackground = (HBRUSH)(COLOR_MENU + 1);
      return __super::WinRegisterClass(pwc);
     }
     ...
    };
    
    void RootWindow::PaintContent(PAINTSTRUCT *pps)
    {
     int cxCheck = GetSystemMetrics(SM_CXMENUCHECK);
     int cyCheck = GetSystemMetrics(SM_CYMENUCHECK);
     RECT rc = { 0, 0, cxCheck, cyCheck };
     DrawFrameControl(pps->hdc, &rc, DFC_MENU, DFCS_MENUCHECK);
    }
    

    This naïvely uses the DrawFrameControl function to draw the menu check mark directly into the paint DC. If you are running with the default Windows XP theme you probably won't notice anything amiss, but switch to the Windows Classic theme and you'll see that the check mark is drawn in black and white even though the Classic menu background color is gray.

    The reason for this is called out in the documentation for DrawFrameControl:

    If uType is either DFC_MENU or DFC_BUTTON and uState is not DFCS_BUTTONPUSH, the frame control is a black-on-white mask (that is, a black frame control on a white background).

    All we get from DrawFrameControl is a monochrome mask. It is our responsibility to colorize it as necessary. To do this, we draw the mask into a monochrome bitmap, and then use the BitBlt function to colorize it. Recall that when blitting from a monochrome bitmap to a color bitmap, the color black in the source bitmap becomes the destination DC's text color, and the color white in the source bitmap becomes the destination DC's background color.

    void RootWindow::PaintContent(PAINTSTRUCT *pps)
    {
     HDC hdcMem = CreateCompatibleDC(pps->hdc);
     if (hdcMem) {
      int cxCheck = GetSystemMetrics(SM_CXMENUCHECK);
      int cyCheck = GetSystemMetrics(SM_CYMENUCHECK);
      HBITMAP hbmMono = CreateBitmap(cxCheck, cyCheck, 1, 1, NULL);
      if (hbmMono) {
       HBITMAP hbmPrev = SelectBitmap(hdcMem, hbmMono);
       if (hbmPrev) {
        RECT rc = { 0, 0, cxCheck, cyCheck };
        DrawFrameControl(hdcMem, &rc, DFC_MENU, DFCS_MENUCHECK);
        COLORREF clrTextPrev = SetTextColor(pps->hdc,
                                         GetSysColor(COLOR_MENUTEXT));
        COLORREF clrBkPrev = SetBkColor(pps->hdc,
                                             GetSysColor(COLOR_MENU));
        BitBlt(pps->hdc, 0, 0, cxCheck, cyCheck,
               hdcMem, 0, 0, SRCCOPY);
        SetBkColor(pps->hdc, clrBkPrev);
        SetTextColor(pps->hdc, clrTextPrev);
        SelectBitmap(hdcMem, hbmPrev);
       }
       DeleteObject(hbmMono);
      }
      DeleteDC(hdcMem);
     }
    }
    

    The key steps here are (1) drawing into a temporary monochrome bitmap to generate the mask, (2) setting the text and background colors of the destination DC, (3) using BitBlt to do the color mapping. The rest of the function is just boring bookkeeping.

    Observe that the checkmark's colors now match the system menu colors because we set them as the text and background colors for the mono-to-color blit.

    Armed with this knowledge, perhaps you can help this person, who is trying to draw the menu check marks transparently. I can think of two different solutions off the top of my head.

  • The Old New Thing

    Microsoft Company Picnic 2005

    • 14 Comments

    This weekend, it was Microsoft's turn to rent Mountain Meadows Farm for the company picnic. As I noted last year, the picnic is put on by a company that just puts on company picnics all summer. In addition to Microsoft, they also do Alaska Air, Honeywell, T-Mobile, and Amazon.

    I decided to bicycle to the picnic this year. The route was approximately 25 miles from Marymoor Park (6046 W Lake Sammamish Pkwy NW, Redmond WA) to Mountain Meadows Farm (10106 422nd Ln SE, North Bend, WA: three out of four web-based mapping services can find the farm given the address) along State Highway 202. According to the Fat Cyclist, Highway 202 from Redmond to Fall City "is the very definition of 'rolling'." I'd have to agree. I was able to maintain 18–20mph without too much difficulty. (That little statistic lets all you cyclists figure out how much of a patsy I am in the bicycling scheme of things.) But of course it's the part right after Fall City that is the doozy, because that's where you climb from the base of Snoqualmie Falls to the top.

    A digression on Fat Cyclist: His web site is a riot. Read his commuting tips and his description of bicycling etiquette for starters. As a former Fat Cyclist myself, I wish him all the best. You can do it!

    When I started out bicycling, I had a jersey but just wore plain shorts even though I had a pair of those stupid-looking bicycle shorts. I didn't wear them because I didn't want to look like a poser. In my plain shorts, I would wave to other cyclists but they would just ignore me. One day I decided to wear the stupid-looking shorts and the world changed. Other cyclists would wave to me first! I suddenly had street cred. It was all about the shorts.

    Oh, right, biking to the company picnic. At Marymoor Park, I met up with a friend who had already put about fifty miles on his bicycle that morning just getting to the park. (So either he was already tired and I could look like a non-patsy, or he was all warmed up and I was toast. I couldn't tell which way it was going to go...) We headed out together, and about 15 miles into the trip, I blew a tire just before reaching Fall City.

    We pulled over and I pulled out my spare tube. Replaced the tube, put the wheel back on the bicycle, started pumping it up, and oh look the Presta valve snapped off inside the pump. Well that sucks, because I carried only one spare tube.

    Plan B: My friend pulled out his patch repair kit to fix the hole in the original tube, but, alas, the glue had dried up. We had patches but no rubber cement.

    Plan C: We called another friend who lives eight miles away, and he arrived with a patch kit. That patch kit had glue! Unfortunately, the glue didn't take.

    Plan D: Fortunately, he also brought a glueless patch kit. We cleaned the tube of glue and applied the glueless patch. It seemed to hold, though there was a little leakage when the tube was over-inflated. Since we didn't really have any options left regarding patch kits, we decided to go with it and see what happens.

    What happened is that the patch didn't hold and we pulled over in Fall City just a few hundred meters later.

    Plan E: We called my friend's girlfriend who was originally going to meet us at the picnic, telling her where we were and asking her to come pick us up. She ultimately arrived, we loaded the bicycles onto the carrier, and drove the rest of the way to the company picnic.

    Clearly the bicycling gods were trying to tell me something. I apologized to my friend for ruining the ride, but he pointed out that if a spare tube and three patch kits can't fix it, you can't really be blamed for a blown tube.

    At the picnic, we met up with our mutual friend Wendy and hung out, along the way squeezing in a game of what seemed like "multi-ball four-way soccer dodgeball in an inflatable moon walk". It made no sense, but that was probably the point. Wendy didn't join us; she stayed outside and took several "butt pictures", according to her description. I'm not sure I want to see those pictures...

    Curiously, we didn't experience the twenty-minute wait for water that JVert warned about... Probably because we didn't make it to the picnic until 2pm, by which time the lunch crush was over.

  • The Old New Thing

    Rendering standard Windows elements

    • 15 Comments

    The DrawFrameControl function allows you to render standard Windows elements in your custom controls. Let's start by simply rendering a selected radio button. Start with our new scratch program and make this very simple change:

    class RootWindow : public Window
    {
     ...
    protected:
     void PaintContent(PAINTSTRUCT *pps);
     ...
    };
    
    void RootWindow::PaintContent(PAINTSTRUCT *pps)
    {
     RECT rc = { 0, 0, 32, 32 };
     DrawFrameControl(pps->hdc, &rc, DFC_BUTTON,
                      DFCS_BUTTONRADIO | DFCS_CHECKED);
    }
    

    When you run the program, you'll see a little radio button in the corner. Woo-hoo.

    You might also notice that it's an unthemed radio button. To get a themed radio button, you need to use the theme-drawing functions defined in the uxtheme.h header file. Let's make the following further changes:

    class RootWindow : public Window
    {
     ...
    protected:
     void OpenTheme() { m_htheme = OpenThemeData(m_hwnd, L"Button"); }
     void CloseTheme()
     {
      if (m_htheme) { CloseThemeData(m_htheme); m_htheme = NULL; }
     }
     RootWindow() : m_htheme(NULL) { }
     ~RootWindow() { CloseTheme(); }
     ...
    };
    
    LRESULT RootWindow::OnCreate()
    {
     OpenTheme();
     return 0;
    }
    
    void RootWindow::PaintContent(PAINTSTRUCT *pps)
    {
     RECT rc = { 0, 0, 32, 32 };
     if (m_htheme) {
      DrawThemeBackground(m_htheme, pps->hdc,
                          BP_RADIOBUTTON, RBS_CHECKEDNORMAL,
                          &rc, NULL);
     } else {
      DrawFrameControl(pps->hdc, &rc, DFC_BUTTON,
                       DFCS_BUTTONRADIO | DFCS_CHECKED);
     }
    }
    
    LRESULT RootWindow::HandleMessage(...)
    {
     ...
      case WM_THEMECHANGED:
       CloseTheme();
       OpenTheme();
       break;
     ...
    }
    

    This new version attempts to open the "Button" theme for the window when the window is created. If themes are not enabled, then this call will fail. When it comes time to draw, we see whether we have a theme available. If so, then we use the DrawThemeBackground function to draw it; otherwise, we draw it the unthemed way. Of course, we close the theme handle at destruction, and we also refresh the theme handle if the user changes the theme.

    If you run this new program with themes enabled, then you will get the pretty themed radio button instead of the old-fashioned unthemed radio button.

    Next time, we'll look at the trickier menu bitmaps.

  • The Old New Thing

    Does Windows have a limit of 2000 threads per process?

    • 30 Comments

    Often I see people asking why they can't create more than around 2000 threads in a process. The reason is not that there is any particular limit inherent in Windows. Rather, the programmer failed to take into account the amount of address space each thread uses.

    A thread consists of some memory in kernel mode (kernel stacks and object management), some memory in user mode (the thread environment block, thread-local storage, that sort of thing), plus its stack. (Or stacks if you're on an Itanium system.)

    Usually, the limiting factor is the stack size.

    #include <stdio.h>
    #include <windows.h>
    
    DWORD CALLBACK ThreadProc(void*)
    {
     Sleep(INFINITE);
     return 0;
    }
    
    int __cdecl main(int argc, const char* argv[])
    {
    int i;
     for (i = 0; i < 100000; i++) {
      DWORD id;
      HANDLE h = CreateThread(NULL, 0, ThreadProc, NULL, 0, &id);
      if (!h) break;
      CloseHandle(h);
     }
     printf("Created %d threads\n", i);
     return 0;
    }
    

    This program will typically print a value around 2000 for the number of threads.

    Why does it give up at around 2000?

    Because the default stack size assigned by the linker is 1MB, and 2000 stacks times 1MB per stack equals around 2GB, which is how much address space is available to user-mode programs.

    You can try to squeeze more threads into your process by reducing your stack size, which can be done either by tweaking linker options or manually overriding the stack size passed to the CreateThread functions as described in MSDN.

      HANDLE h = CreateThread(NULL, 4096, ThreadProc, NULL,
                   STACK_SIZE_PARAM_IS_A_RESERVATION, &id);
    

    With this change, I was able to squeak in around 13000 threads. While that's certainly better than 2000, it's short of the naive expectation of 500,000 threads. (A thread is using 4KB of stack in 2GB address space.) But you're forgetting the other overhead. Address space allocation granularity is 64KB, so each thread's stack occupies 64KB of address space even if only 4KB of it is used. Plus of course you don't have free reign over all 2GB of the address space; there are system DLLs and other things occupying it.

    But the real question that is raised whenever somebody asks, "What's the maximum number of threads that a process can create?" is "Why are you creating so many threads that this even becomes an issue?"

    The "one thread per client" model is well-known not to scale beyond a dozen clients or so. If you're going to be handling more than that many clients simultaneously, you should move to a model where instead of dedicating a thread to a client, you instead allocate an object. (Someday I'll muse on the duality between threads and objects.) Windows provides I/O completion ports and a thread pool to help you convert from a thread-based model to a work-item-based model.

    Note that fibers do not help much here, because a fiber has a stack, and it is the address space required by the stack that is the limiting factor nearly all of the time.

  • The Old New Thing

    What is this "web site" thing you are talking about?

    • 29 Comments

    One reaction I've seen when people learn about all the compatibility work done in the Windows 95 kernel is to say,

    Why not add code to the installer wizard [alas, page is now 404] which checks to see if you're installing SimCity and, if so, informs you of a known design flaw, then asks you to visit Electronic Arts' webpage for a patch?

    Let's ignore the issue of the "installer wizard"; most people do not go through the Add and Remove Programs control panel to install programs, so any changes to that control panel wouldn't have helped anyway.

    But what about detecting that you're running SimCity and telling you to get a patch from Electronic Arts' web site?

    Remember, this was 1993. Almost nobody had web sites. The big thing was the "Information Superhighway". (Remember that? I don't think it ever got built; the Internet sort of stole its thunder.) If you told somebody, "Go to Electronic Arts' web site and download a patch", you'd get a blank stare. What's a "web site"? How do I access that from Prodigy? I don't have a modem. Can you mail me their web site?

    In Windows XP, when Windows detects that you're running a program with which it is fundamentally incompatible, you do get a pop-up window directing you to the company's web site. But that's because it's now 2005 and even hermits living in caves have email addresses.

    In 1993, things were a little different.

    (Heck, even by 1995 things most people did not have Internet access and those few that did used modems. Requiring users to obtain Internet access in order to set the computer clock via NTP would have been rather presumptuous.)

  • The Old New Thing

    When the normal window destruction messages are thrown for a loop

    • 17 Comments

    Last time, I alluded to weirdness that can result in the normal cycle of destruction messages being thrown out of kilter.

    Commenter Adrian noted that the WM_GETMINMAXINFO message arrives before WM_NCCREATE for top-level windows. This is indeed unfortunate but (mistake or not) it's been that way for over a decade and changing it now would introduce serious compatibility risk.

    But that's not the weirdness I had in mind.

    Some time ago I was helping to debug a problem with a program that was using the ListView control, and the problem was traced to the program subclassing the ListView control and, through a complicated chain of C++ objects, ending up attempting to destroy the ListView control while it was already in the process of being destroyed.

    Let's take our new scratch program and illustrate what happens in a more obvious manner.

    class RootWindow : public Window
    {
    public:
     RootWindow() : m_cRecurse(0) { }
     ...
    private:
     void CheckWindow(LPCTSTR pszMessage) {
      OutputDebugString(pszMessage);
      if (IsWindow(m_hwnd)) {
       OutputDebugString(TEXT(" - window still exists\r\n"));
      } else {
       OutputDebugString(TEXT(" - window no longer exists\r\n"));
      }
     }
    private:
     HWND m_hwndChild;
     UINT m_cRecurse;
     ...
    };
    
    LRESULT RootWindow::HandleMessage(
                              UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
     ...
      case WM_NCDESTROY:
       CheckWindow(TEXT("WM_NCDESTROY received"));
       if (m_cRecurse < 2) {
        m_cRecurse++;
        CheckWindow(TEXT("WM_NCDESTROY recursing"));
        DestroyWindow(m_hwnd);
        CheckWindow(TEXT("WM_NCDESTROY recursion returned"));
       }
       PostQuitMessage(0);
       break;
    
      case WM_DESTROY:
       CheckWindow(TEXT("WM_DESTROY received"));
       if (m_cRecurse < 1) {
        m_cRecurse++;
        CheckWindow(TEXT("WM_DESTROY recursing"));
        DestroyWindow(m_hwnd);
        CheckWindow(TEXT("WM_DESTROY recursion returned"));
       }
       break;
      ...
    }
    

    We add some debug traces to make it easier to see what is going on. Run the program, then close it, and watch what happens.

    WM_DESTROY received - window still exists
    WM_DESTROY recursing - window still exists
    WM_DESTROY received - window still exists
    WM_NCDESTROY received - window still exists
    WM_NCDESTROY recursing - window still exists
    WM_DESTROY received - window still exists
    WM_NCDESTROY received - window still exists
    WM_NCDESTROY recursion returned - window no longer exists
    Access violation - code c0000005
    eax=00267160 ebx=00000000 ecx=00263f40 edx=7c90eb94 esi=00263f40 edi=00000000
    eip=0003008f esp=0006f72c ebp=0006f73c iopl=0         nv up ei ng nz na pe cy
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000283
    0003008f ??               ???
    

    Yikes! What happened?

    When you clicked the "X" button, this started the window destruction process. As is to be expected, the window received a WM_DESTROY message, but the program responds to this by attempting to destroy the window again. Notice that IsWindow reported that the window still exists at this point. This is true: The window does still exist, although it happens to be in the process of being destroyed. In the original scenario, the code that destroyed the window went something like

    if (IsWindow(hwndToDestroy)) {
     DestroyWindow(hwndToDestroy);
    }
    

    At any rate, the recursive call to DestroyWindow caused a new window destruction cycle to begin, nested inside the first one. This generates a new WM_DESTROY message, followed by a WM_NCDESTROY message. (Notice that this window has now received two WM_DESTROY messages!) Our bizarro code then makes yet another recursive call to DestroyWindow, which starts a third window destruction cycle. The window gets its third WM_DESTROY message, then its second WM_NCDESTROY message, at which point the second recursive call to DestroyWindow returns. At this point, the window no longer exists: DestroyWindow has destroyed the window.

    And that's why we crash. The base Window class handles the WM_NCDESTROY message by destroying the instance variables associated with the window. Therefore, when the innermost DestroyWindow returns, the instance variables have been thrown away. Execution then resumes with the base class's WM_NCDESTROY handler, which tries to access the instance variables and gets heap garbage, and then makes the even worse no-no of freeing memory that is already freed, thereby corrupting the heap. It is here that we crash, attempting to call the virtual destructor on an already-destructed object.

    I intentionally chose to use the new scratch program (which uses C++ objects) instead of the classic scratch program (which uses global variables) to highlight the fact that after the recursive DestroyWindow call, all the instance variables are gone and you are operating on freed memory.

    Moral of the story: Understand your window lifetimes and don't destroy a window that you know already to be in the process of destruction.

  • The Old New Thing

    What is the difference between WM_DESTROY and WM_NCDESTROY?

    • 16 Comments

    There are two window messages closely-associated with window destruction, the WM_DESTROY message and the WM_NCDESTROY message. What's the difference?

    The difference is that the WM_DESTROY message is sent at the start of the window destruction sequence, whereas the WM_NCDESTROY message is sent at the end. This is an important distinction when you have child windows. If you have a parent window with a child window, then the message traffic (in the absence of weirdness) will go like this:

    hwnd = parent, uMsg = WM_DESTROY
    hwnd = child, uMsg = WM_DESTROY
    hwnd = child, uMsg = WM_NCDESTROY
    hwnd = parent, uMsg = WM_NCDESTROY
    

    Notice that the parent receives the WM_DESTROY before the child windows are destroyed, and it receives the WM_NCDESTROY message after they have been destroyed.

    Having two destruction messages, one sent top-down and the other bottom-up, means that you can perform clean-up appropriate to a particular model when handling the corresponding message. If there is something that must be cleaned up top-down, then you can use the WM_DESTROY message, for example.

    The WM_NCDESTROY is the last message your window will receive (in the absence of weirdness), and it is therefore the best place to do "final cleanup". This is why our new scratch program waits until WM_NCDESTROY to destroy its instance variables.

    These two destruction messages are paired with the analogous WM_CREATE and WM_NCCREATE messages. Just as WM_NCDESTROY is the last message your window receives, the WM_NCCREATE message is the first message, so that's a good place to create your instance variables. Note also that if you cause the WM_NCCREATE message to return failure, then all you will get is WM_NCDESTROY; there will be no WM_DESTROY since you never got the corresponding WM_CREATE.

    What's this "absence of weirdness" I keep alluding to? We'll look at that next time.

    [Typos corrected, 9:30am]

Page 370 of 453 (4,523 items) «368369370371372»