July, 2003

  • The Old New Thing

    Why do you have to click the Start button to shut down?

    • 22 Comments

    Short answer: The same reason you turn the ignition key to shut off your car.

    Long answer: Back in the early days, the taskbar didn't have a Start button. (In a future history column, you'll learn that back in the early days, the taskbar wasn't called the taskbar.)

    Instead of the Start button, there were three buttons in the lower left corner. One was the "System" button (icon: the Windows flag), one was the "Find" button (icon: an eyeball), and the third was the "Help" button (icon: a question mark). "Find" and "Help" are self-explanatory. The "System" button gave you this menu:

     Run...
     Task List...

     Arrange Desktop Icons  
     Arrange Windows 4

     Shutdown Windows

    ("Arrange Windows" gave you options like "Cascade", "Tile Horizontally", that sort of thing.)

    Of course, over time, the "Find" and "Help" buttons eventually joined the "System" button menu and the System button menu itself gradually turned into the Windows 95 Start menu.

    But one thing kept getting kicked up by usability tests: People booted up the computer and just sat there, unsure what to do next.

    That's when we decided to label the System button "Start".

    It says, "You dummy. Click here." And it sent our usability numbers through the roof, because all of a sudden, people knew what to click when they wanted to do something.

    So why is "Shut down" on the Start menu?

    When we asked people to shut down their computers, they clicked the Start button.

    Because, after all, when you want to shut down, you have to start somewhere.

    (Besides, if we also had a "Shut down" button next to the Start button, everybody would be demanding that we get rid of it to save valuable screen real estate.)

  • The Old New Thing

    The scratch program

    • 23 Comments

    Occasionally, there is need to illustrate a point with a full program. To avoid reproducing the boring parts of the program, let's agree on using the following template for our sample programs.

    For expository purposes, I won't use a C++ class. I'll just keep all my variables global. In a real program, of course, instance data would be attached to the window instead of floating globally.

    #define STRICT
    #include <windows.h>
    #include <windowsx.h>
    #include <ole2.h>
    #include <commctrl.h>
    #include <shlwapi.h>
    
    HINSTANCE g_hinst;                          /* This application's HINSTANCE */
    HWND g_hwndChild;                           /* Optional child window */
    
    /*
     *  OnSize
     *      If we have an inner child, resize it to fit.
     */
    void
    OnSize(HWND hwnd, UINT state, int cx, int cy)
    {
        if (g_hwndChild) {
            MoveWindow(g_hwndChild, 0, 0, cx, cy, TRUE);
        }
    }
    
    /*
     *  OnCreate
     *      Applications will typically override this and maybe even
     *      create a child window.
     */
    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
        return TRUE;
    }
    
    /*
     *  OnDestroy
     *      Post a quit message because our application is over when the
     *      user closes this window.
     */
    void
    OnDestroy(HWND hwnd)
    {
        PostQuitMessage(0);
    }
    
    /*
     *  PaintContent
     *      Interesting things will be painted here eventually.
     */
    void
    PaintContent(HWND hwnd, PAINTSTRUCT *pps)
    {
    }
    
    /*
     *  OnPaint
     *      Paint the content as part of the paint cycle.
     */
    void
    OnPaint(HWND hwnd)
    {
        PAINTSTRUCT ps;
        BeginPaint(hwnd, &ps);
        PaintContent(hwnd, &ps);
        EndPaint(hwnd, &ps);
    }
    
    /*
     *  OnPrintClient
     *      Paint the content as requested by USER.
     */
    void
    OnPrintClient(HWND hwnd, HDC hdc)
    {
        PAINTSTRUCT ps;
        ps.hdc = hdc;
        GetClientRect(hwnd, &ps.rcPaint);
        PaintContent(hwnd, &ps);
    
    }
    
    /*
     *  Window procedure
     */
    LRESULT CALLBACK
    WndProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
    {
        switch (uiMsg) {
    
        HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
        HANDLE_MSG(hwnd, WM_SIZE, OnSize);
        HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
        HANDLE_MSG(hwnd, WM_PAINT, OnPaint);
        case WM_PRINTCLIENT: OnPrintClient(hwnd, (HDC)wParam); return 0;
        }
    
        return DefWindowProc(hwnd, uiMsg, wParam, lParam);
    }
    
    BOOL
    InitApp(void)
    {
        WNDCLASS wc;
    
        wc.style = 0;
        wc.lpfnWndProc = WndProc;
        wc.cbClsExtra = 0;
        wc.cbWndExtra = 0;
        wc.hInstance = g_hinst;
        wc.hIcon = NULL;
        wc.hCursor = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wc.lpszMenuName = NULL;
        wc.lpszClassName = TEXT("Scratch");
    
        if (!RegisterClass(&wc)) return FALSE;
    
        InitCommonControls();               /* In case we use a common control */
    
        return TRUE;
    }
    
    int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                       LPSTR lpCmdLine, int nShowCmd)
    {
        MSG msg;
        HWND hwnd;
    
        g_hinst = hinst;
    
        if (!InitApp()) return 0;
    
        if (SUCCEEDED(CoInitialize(NULL))) {/* In case we use COM */
    
            hwnd = CreateWindow(
                TEXT("Scratch"),                /* Class Name */
                TEXT("Scratch"),                /* Title */
                WS_OVERLAPPEDWINDOW,            /* Style */
                CW_USEDEFAULT, CW_USEDEFAULT,   /* Position */
                CW_USEDEFAULT, CW_USEDEFAULT,   /* Size */
                NULL,                           /* Parent */
                NULL,                           /* No menu */
                hinst,                          /* Instance */
                0);                             /* No special parameters */
    
            ShowWindow(hwnd, nShowCmd);
    
            while (GetMessage(&msg, NULL, 0, 0)) {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
    
            CoUninitialize();
        }
    
        return 0;
    }
    
    

    Notice that all painting gets funneled through the PaintContent function. This allows us to route the WM_PRINTCLIENT message through the same paint function, which has as an immediate consequence the ability to animate the window with AnimateWindow. This will also prove useful for printing high-resolution screenshots.

    Other than the trickiness with painting, there really isn't anything here that you shouldn't already know. The point of this program is to be a template for future programs.

    My first mission will be an eight-part series on scrollbars.

    That's right. Scrollbars.

    I can't believe I have an eight-part series on scrollbars. And you probably can't believe you're reading about it.

  • The Old New Thing

    Why doesn't Windows have an "expert mode"?

    • 15 Comments

    We often get requests like this:

    There should be a slider bar somewhere, say on the Performance Tab, that ranges from Novice to Advanced. At the highest level, all the geek settings are turned on. At the Novice level, all the settings for beginners are turned on. In between, we can gradually enable stuff.

    We've been trying to do something like this since even before Windows 95, and it doesn't work.

    It doesn't work because somebody who is a whiz at Excel will rate themselves as Advanced even though they can't tell a CPU from a box of Cracker Jacks.

    They're not stupid. They really are advanced users. Just not advanced at the skill we're asking them about.

    And before you go mocking the non-geeks: Even geeks don't know everything. I know a lot about GUI programming, but I only know a little about disk partitioning, and I don't know squat about Active Directory. So am I an expert?

  • The Old New Thing

    Tweak UI 2.10

    • 11 Comments
    What are you talking about?
    Tweak UI is part of the Windows XP PowerToys. It was recently updated to version 2.10.
    What OS is required?
    Windows XP Service Pack 1 (or higher) or Windows Server 2003 (all versions). Not supported: Windows XP RTM, Windows 2000, NT 4, Windows 95, 98, or Me.
    Why didn't you make a big announcement? I didn't find out about it until somebody told me.
    I don't control the announcements page. I figured word would get out - and it did.
    Why is the new Tweak UI so much smaller than the old one?
    Leaner installer program.
    Are you always this terse?
    I'm not sure yet; I'm new at this.
  • The Old New Thing

    Scrollbars, part 2

    • 1 Comments

    Managing scrollbars is not hard in principle. The basic idea is not that difficult, but there are a lot of small details that need to be ironed out. If you don't get the details just right, your program will feel odd in a strange way that you often can't pinpoint, much like a subtle background hum that makes you feel uneasy without realizing it. Getting the details right is important to making your program feel crisp and clean.

    The base program

    Let's start with a basic program and gradually add scrollbar features to it. The basic program merely displays one hundred numbered lines. Add these variables to the scratch program:

    HFONT g_hfList;         /* Font for list */
    int g_cyLine;           /* Height of each line */
    int g_cItems = 100;     /* Number of items */
    

    and add these functions to the scratch program:

    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
        /* Create the font we use for the list */
        LOGFONT lf;
        SystemParametersInfo(SPI_GETICONTITLELOGFONT, sizeof(lf), &lf, 0);
        g_hfList = CreateFontIndirect(&lf);
        if (!g_hfList) return 0;
    
        /* Compute the height for each line */
        HDC hdc = GetDC(hwnd);
        HFONT hfPrev = SelectFont(hdc, g_hfList);
        SelectFont(hdc, hfPrev);
        SIZE siz;
        GetTextExtentPoint(hdc, TEXT("0"), 1, &siz);
        g_cyLine = siz.cy;
    
        ReleaseDC(hwnd, hdc);
    
        return 1;
    }
    
    void
    OnDestroy(HWND hwnd)
    {
        DeleteObject(g_hfList);
        PostQuitMessage(0);
    }
    
    /* This is a separate function for now; you'll see why later */
    void
    PaintSimpleContent(HWND hwnd, PAINTSTRUCT *pps)
    {
        HFONT hfPrev = SelectFont(pps->hdc, g_hfList);  /* Use the right font */
        for (int i = 0; i < g_cItems; i++) {            /* Print all the lines */
            char szLine[256];
            int cch = wsprintf(szLine, "This is line %d", i);
            TextOut(pps->hdc, 0, i * g_cyLine, szLine, cch);
        }
    
        SelectFont(pps->hdc, hfPrev);
    }
    
    void
    PaintContent(HWND hwnd, PAINTSTRUCT *pps)
    {
        PaintSimpleContent(hwnd, pps);
    }
    

    We now have a base program upon which to build.

    I'm sorry this is taking so long to get off the ground. But things finally get interesting in part 4, honest.

  • The Old New Thing

    Why doesn't the new Start menu use Intellimenus in the All Programs list?

    • 8 Comments

    Common request:

    I want to be able to turn on personalized menus (Intellimenus) when in XP Start Menu mode.

    Imagine if Intellimenus were enabled with the XP Start Menu.

    You use 5 apps; the rest are not used much. (Studies show that 5 is the typical number of unique applications users run on a regular basis. All the rest are rare.)

    Those 5 apps are on the MFU.

    You decide today you want to run some other app, one of those other apps that you run rarely.

    You click All Programs.

    You can't find the app because it got chevroned away. It got chevroned away because it's a rare app, by definition ... if it were a popular app it would be on the MFU already!

    If you are a naive user, you say "Hey, who uninstalled all my apps? It's missing from All Programs!" It's kind of a misnomer to call it "All Programs" when in fact it doesn't show all your programs.

    If you are an experienced user, you say, "Sigh, why do I have to keep clicking this chevron? The whole reason I'm going to 'All Programs' is that I want to run an app I haven't run in a long time, duh. The chevrons should be pre-expanded, save me a click!"

    In other words, if we had Intellimenus enabled on All Programs, it would just show you your MFU again, since the MFU and Intellimenus are both showing the same information, just in different ways. That's clearly pointless.

    Think of "All Programs" as a really big chevron. The MFU is the collapsed version. All Programs is the expanded version.

  • The Old New Thing

    Scrollbars, part 4: Adding a proportional scrollbar

    To obtain a proportional scrollbar, you need to tell Windows the minimum and maximum values covered by the scrollbar, the current scrollbar position, and the size of the scrollbar thumb (called the "page size"). One annoyance of the way scrollbars are set up is that the maximum value is attainable. This differs from the way GDI manages dimensions, where the range is exclusive of the endpoint. As a result, there will be occasional "-1"s sprinkled through the code to compensate for the fact that scrollbars include rather than exclude their endpoints.

    To do this, we need a few more variables.

    int g_yOrigin;              /* Scrollbar position */
    int g_cLinesPerPage;        /* Number of lines per page */
    

    The name of the variable g_yOrigin will become apparent later.

    Next comes the helper function that is at the center of the scrollbar action. Given the desired position of the scrollbar, it sanitizes the value, scrolls the window contents as necessary, and sets the scrollbar parameters to match the new state of the window.

    void ScrollTo(HWND hwnd, int pos)
    {
        /*
         *  Keep the value in the range 0 .. (g_cItems - g_cLinesPerPage).
         */
        pos = max(pos, 0);
        pos = min(pos, g_cItems - g_cLinesPerPage);
    
        /*
         *  Scroll the window contents accordingly.
         */
        ScrollWindowEx(hwnd, 0, (g_yOrigin - pos) * g_cyLine,
                       NULL, NULL, NULL, NULL,
                       SW_ERASE | SW_INVALIDATE);
    
        /*
         *  Now that the window has scrolled, a new item is at the top.
         */
        g_yOrigin = pos;
    
        /*
         *  And make the scrollbar look like what we think it is.
         */
        SCROLLINFO si;
        si.cbSize = sizeof(si);
        si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
        si.nPage = g_cLinesPerPage;
        si.nMin = 0;
        si.nMax = g_cItems - 1;     /* endpoint is inclusive */
        si.nPos = g_yOrigin;
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
    }
    

    Sometimes, we merely want to make a relative change to the scrollbar position.

    void ScrollDelta(HWND hwnd, int dpos)
    {
        ScrollTo(hwnd, g_yOrigin + dpos);
    }
    

    When the window changes size, we need to recompute the number of items that fit on one page. This in turn may require that the scrollbar thumb position be adjusted, so we also perform a dummy scroll, which triggers all the sanity-check computations.

    void OnSize(HWND hwnd, UINT state, int cx, int cy)
    {
        g_cLinesPerPage = cy / g_cyLine;
        ScrollDelta(hwnd, 0);
    }
    

    The WM_VSCROLL handler is pretty much self-explanatory. When scrolling by lines or pages, we make a relative change in the appropriate direction and magnitude. When the user drags the thumb, we move directly to the specified location. And when the user drags to the top or bottom of the scrollbar, we peg at one or the other extremum.

    void OnVscroll(HWND hwnd, HWND hwndCtl, UINT code, int pos)
    {
        switch (code) {
        case SB_LINEUP:         ScrollDelta(hwnd, -1); break;
        case SB_LINEDOWN:       ScrollDelta(hwnd, +1); break;
        case SB_PAGEUP:         ScrollDelta(hwnd, -g_cLinesPerPage); break;
        case SB_PAGEDOWN:       ScrollDelta(hwnd, +g_cLinesPerPage); break;
        case SB_THUMBPOSITION:  ScrollTo(hwnd, pos); break;
        case SB_THUMBTRACK:     ScrollTo(hwnd, pos); break;
        case SB_TOP:            ScrollTo(hwnd, 0); break;
        case SB_BOTTOM:         ScrollTo(hwnd, MAXLONG); break;
        }
    }
    

    And, of course, we need to hook up our message handler into the message loop.

        /* Add to WndProc */
        HANDLE_MSG(hwnd, WM_VSCROLL, OnVscroll);
    

    Finally, we need to make our paint handler aware of the scrollbar. Fortunately, through the magic of GDI transforms, we can accomplish this without having to change the PaintSimpleContent function at all.

    void PaintContent(HWND hwnd, PAINTSTRUCT *pps)
    {
        POINT ptOrgPrev;
        OffsetRect(&pps->rcPaint, 0, g_yOrigin * g_cyLine);
        GetWindowOrgEx(pps->hdc, &ptOrgPrev);
        SetWindowOrgEx(pps->hdc, ptOrgPrev.x, ptOrgPrev.y + g_yOrigin * g_cyLine, NULL);
    
        PaintSimpleContent(hwnd, pps);
    
        SetWindowOrgEx(pps->hdc, ptOrgPrev.x, ptOrgPrev.y, NULL);
    }
    

    By changing the window origin, the PaintSimpleContent function continues to believe that there is no scrollbar at all. It blithely draws item zero at (0, 0), but through the magic of GDI transforms, the pixels actually appear at the new origin location.

    Now you see why the variable is named g_yOrigin.

    Exercise:  There is a latent bug in OnVscroll.  Explain what it is and fix it.

  • The Old New Thing

    More terse Q&A on Tweak UI 2.10

    • 2 Comments
    I'm going to try to alternate between programming entries (where I actually try to teach something) and random entries (where I get to spout off or go into storytelling mode). So here's another random entry.

    Why does Tweak UI put up a totally incomprehensible error message ("Cannot locate entrypoint GetDllDirectoryW in Kernel32.dll") when I try to run it on an unsupported OS?

    To make sure there is absolutely no way of running it on an unsupported OS.  From experience, I've learned that people would run Tweak UI on a toaster if they could. If I used a simple runtime check, somebody would just override it. So instead I made the dependency on Windows XP SP1 and Windows Server 2003 so strong that no amount of patching would get it to work, because the block is being done by the OS program loader.  Not a single byte of Tweak UI has even run at this point, so you can NOP out anything you like, it won't get the program to run.

    Tweak UI has a bad default for the X-Mouse autoraise delay.

    Remember, Tweak UI merely provides an interface to existing functionality. I can't go changing the defaults; the defaults aren't mine to change. (In this case, the default comes from SystemParametersInfo(SPI_GETACTIVEWNDTRKTIMEOUT).)

  • The Old New Thing

    Scrollbars, part 3: Optimizing the paint cycle

    • 1 Comments

    Observe that we paint all 100 lines in our paint handler, even though most of them aren't visible. This is a problem if there are a large number of items, or if painting an item is time-consuming.

    So instead, we optimize our paint cycle so as to paint only the elements which intersect the paint rectangle.

    void
    PaintSimpleContent(HWND hwnd, PAINTSTRUCT *pps)
    {
        HFONT hfPrev = SelectFont(pps->hdc, g_hfList);  /* Use the right font */
    
        int iMin = max(pps->rcPaint.top / g_cyLine, 0);
        int iMax = min((pps->rcPaint.bottom + g_cyLine - 1) / g_cyLine, g_cItems);
    
        for (int i = iMin; i < iMax; i++) {
            char szLine[256];
            int cch = wsprintf(szLine, "This is line %d", i);
            TextOut(pps->hdc, 0, i * g_cyLine, szLine, cch);
        }
    
        SelectFont(pps->hdc, hfPrev);
    }
    

    Exercise: Explain the formulas for iMin and iMax. Explain why the seemingly equivalent formula

        int iMax = min((pps->rcPaint.bottom - 1) / g_cyLine + 1, g_cItems);
    

    is wrong. Then explain why it doesn't really matter too much.

  • The Old New Thing

    Answer to yesterday's exercise

    • 5 Comments

    iMin is the lowest-index element which intersects the paint rectangle, so a simple truncating division produces the desired index.

    The formula for iMax can be interpreted two ways. One is that it is the roundup of the first invisible line. Recall the rectangles are exclusive of the endpoint, so rcPaint.bottom is actually the first row outside the rectangle. Since we want the first element that is completely outside the rectangle, we must round up.

    A second interpretation begins with the seemingly equivalent formula. First, the expression

        (pps->rcPaint.bottom - 1) / g_cyLine
    

    computes the index of the last visible item. By adding unity, we get the index of the first invisible item.

    In both cases, we do not allow the computed value to exceed g_cItems so we don't try to draw items that don't exist.

    The answer to the next question is that the seemingly equivalent formula does not work correctly when rcPaint.bottom is zero or negative because the result of integer division is rounded towards zero, which would result in an erroneous computation that the value of iMax should be one instead of zero. If integer divisions were rounded towards negative infinity, then the formula would be correct.

    And the answer to the final question is that the only harm is that we sometimes draw one item that we really didn't need to. In our example, this is not that big a deal since drawing an item is relatively fast. But in cases where drawing an item is expensive, avoiding the drawing of even a single item may prove significant.

    And we'll see in Part 4 that playing with the origin can cause the paint rectangle to end up in odd positions.

Page 1 of 1 (10 items)