• 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]

  • The Old New Thing

    Dinner at the Herbfarm in Woodinville

    • 9 Comments

    As part of the going-away festivities for my friend, a group of us went to The Herbfarm, the local restaurant referenced in Clue I of Puzzle #3.

    The restaurant is nestled in the Sammamish Valley, right next to the Willows Lodge resort and its restaurant, The Barking Frog. Less than a kilometer down the road is the Chateau Ste. Michelle Winery. All the high-falutin' wine-snob destinations in one convenient location. (Right behind is the Redhook Brewery, if you tire of the whole urbane ambience and just want a beer and a sandwich.)

    Actually, the word "restaurant" doesn't do the Herbfarm justice. It's really a "total dining experience". The evening begins at 4pm with a tour of the herb garden. After heading inside, you are introduced to the evening's menu and the restaurant staff. (This restaurant has a staff mushroom forager!) Once the introductions are complete, the nine-course five-hour meal begins.

    For posterity, I record the menu below.

    A Fete for the Sun
    The Herbfarm • Sunday, July 24, 2005

    From the Water's Edge
    Paddlefish Caviar on Cucumber Gelée
    Westcott Bay Mussel Skewer
    Razor Clams with Sea Beans
    1999 Argyle Oregon Brut

    Spice-Crusted Pacific Albacore
    On a Salad of Beets, Wasabi, and Dill
    2003 Rulo Winery Viognier, Walla Walla

    Dungeness Crab, Chanterelle, & Lamb's Quarter Lasagna
    2004 Dr. Loosen/Chateau Ste. Michelle “Eroica” Riesling

    Troll-Caught Sockeye Salmon
    Slow Roasted in Squash Blossoms
    With Zucchini Strands and Sorrel-Lemon Thyme Sauce
    2004 Chinook Wines Rosé of Cabernet Franc

    Tarragon Ice & Apricot Float

    Three Tastes of Muscovy Duck
    Lavender-Crusted Breast on Onion Pudding
    With Black Currant Sauce
    Duck Confit with Just-Dug Potatoes
    Slow-Braised Leg on Morel Risotto
    2002 Cougar Crest Reserve Syrah, Stellar Vineyard

    An Herbfarm Garden Salad
    With Currants & Sally Jackson Guernsey Cow Cheese

    Sonata of Summer Desserts
    Anise Hyssop Panna Cotta with Raspberries
    Marionberry-Rose Geranium Ice Cream Cone
    Almond-Filled Donut Peach with Lemon Verbena Yogurt Ice Cream

    Brewed Coffees, Teas & Infusions

    A Selection of Small Treats
    Vintage 1916 Barbeito Malvazia Madeira

    Multigrain Rolls & Herbed Foccacia

    Between the duck and salad courses, we stepped outside to feed the pigs.

    The preliminary tour of the herb garden primed me to appreciate the extensive range of herbs employed in the various dishes. (We also learned herb trivia—daylilies are edible, as are zucchini and chive blossoms. I found this part most fascinating, learning about herbs and how the various elements were chosen for the dishes to come.) The garden salad, it seemed, didn't have two leaves from the same plant!

    If you're considering paying the Herbfarm a visit, be warned: You need to make reservations months in advance, and the evening will set you back a pretty penny.

    And we did pay the bill before we left.

  • The Old New Thing

    On paying for your meal upon leaving a restaurant

    • 14 Comments

    Robert Scoble's embarrassment over forgetting to pay a restaurant bill reminds me of an even more embarrassing incident experienced by a component team from the Windows 95 team.

    To celebrate something or other, their team went to lunch at The Salish Lodge, a fine dining establishment. At the end of the meal, everybody thought somebody else was going to handle the bill, and they all walked out as a group. The administrative assistant who made the reservation received a somewhat concerned telephone call from the restaurant when they discovered that a large party just skipped the check. Profuse apologies were extended and the bill was settled over the phone (with what I assume was a very generous tip). I just happened to be in the hallway when this whole thing happened and got to hear the story from the very exasperated administrative assistant shortly after it transpired.

    So remember, folks: Pay the bill before leaving the restaurant. It saves everybody a lot of grief.

  • The Old New Thing

    Marin Alsop to be music director of the Baltimore Symphony Orchestra

    • 16 Comments

    Marin Alsop has been selected to lead the Baltimore Symphony Orchestra. The development has gotten a lot of attention, presumably, because this makes Maestra Alsop (as she prefers to be called) the first woman to be named music director of a major U.S. orchestra. I saw her perform in Seattle earlier this year and thought she did a fine job. If I'd known she'd become a big newsmaker a few months later, I would have taken notes!

    The focus on her "breakthrough accomplishment" just strikes me as odd. I thought we as a society were beyond being concerned about the gender of the best person for the job. I remember somebody asking, "What it like being from the first state in the United States to have your governor and both senators all women?" It never even occurred to me that all three positions were held by women. It struck me as silly as the question, "What it like being from the first state in the United States to have your governor and both senators all have blonde hair?" What kind of answer were they looking for?

    (Curiously, if you ask Google Who is the governor of Washington?, it says the answer is Gary Locke. Don't use Google to cheat on your civics exam yet.)

Page 349 of 431 (4,310 items) «347348349350351»