September, 2003

  • The Old New Thing

    Answers to exercise from Scrollbars Part 11

    • 0 Comments

    Exercise: Why do we use the formula c = a + (b-a)/2 instead of the simpler c = (a+b)/2?

    Answer: To avoid integer overflow in the computation of a+b.

    Here, a and b are window coordinates, and the window can be anywhere. If the window were placed at extreme coordinates like (MAXLONG,MAXLONG), then the arithmetic would overflow and the "midpoint" would be incorrectly computed.

    Note that the alternate formula a+(b-a)/2 is also subject to overflow, this time in the computation of the value b-a. However, in our case, b-a is the width of our window, which is something that we can control.

    Integer overflow was one of the Windows 95 application compatibility bugs that I had to deal with. There was a DOS game that wanted to do a binary search, and instead of using indices, they attempted to average the two pointers together:

    BYTE *low = ...;
    BYTE *high = ...;
    BYTE *mid = ((UINT)low + (UINT)high)/2;
    

    This worked as long as the game was being run under an operating system without virtual memory, because the "low" and "high" pointers would both be comparatively small numbers (nobody had machines with 2GB of RAM), so the sum low+high would not overflow.

    Windows 95 ran these DOS games, but under a DPMI server that supported virtual memory. The DPMI specification permits the server to put memory anywhere, and we put our memory at the high end of the address space.

    This program then overflowed in its attempt to average the two pointers and crashed.

    So be careful how you average two values together. It's harder than you think.
  • The Old New Thing

    Scrollbars part 12: Applying WM_NCCALCSIZE to our scrollbar sample

    • 2 Comments

    Now that we have learned about the intricacies of the WM_NCCALCSIZE message, we can use it to get rid of the flicker in our resizing code. We just take the trick we used above and apply it to the scroll program.

    First, we need to get rid of the bad flickery resize, so return the OnWindowPosChanging function to the version before we tried doing metaphor work:

    BOOL OnWindowPosChanging(HWND hwnd, LPWINDOWPOS pwp)
    {
        if (!(pwp->flags & SWP_NOSIZE)) {
            RECT rc = { 0, 0, pwp->cx, pwp->cy };
            AdjustSizeRectangle(hwnd, WMSZ_BOTTOM, &rc);
            pwp->cy = rc.bottom;
        }
        return 0;
    }
    

    Instead, our work will happen in the WM_NCCALCSIZE handler.

    UINT OnNcCalcSize(HWND hwnd, BOOL fCalcValidRects,
                      NCCALCSIZE_PARAMS *pcsp)
    {
        UINT uRc = (UINT)FORWARD_WM_NCCALCSIZE(hwnd,
                            fCalcValidRects, pcsp, DefWindowProc);
    
        if (fCalcValidRects) {
            //  Give names to these things
            RECT *prcClientNew = &pcsp->rgrc[0];
            RECT *prcValidDst  = &pcsp->rgrc[1];
            RECT *prcValidSrc  = &pcsp->rgrc[2];
            int dpos;
            int pos;
    
            // Did we drag the top edge enough to scroll?
            if (prcClientNew->bottom == prcValidSrc->bottom &&
                g_cyLine &&
                (dpos = (prcClientNew->top - prcValidSrc->top)
                                                / g_cyLine) != 0 &&
                (pos = ClampScrollPos(g_yOrigin + dpos)) != g_yOrigin) {
    
                *prcValidDst = *prcClientNew;
                ScrollTo(hwnd, pos, FALSE);
                prcValidDst->top -= dpos * g_cyLine;
    
                uRc = WVR_VALIDRECTS;
            }
    
        }
        return uRc;
    }
    
        /* Add to WndProc */
        HANDLE_MSG(hwnd, WM_NCCALCSIZE, OnNcCalcSize);
    

    This uses a new helper function which we extracted from the ScrollTo function. (If I had planned this better, this would have been factored out when we first wrote ScrollTo.)

    int ClampScrollPos(int pos)
    {
        /*
         *  Keep the value in the range 0 .. (g_cItems - g_cyPage).
         */
        pos = max(pos, 0);
        pos = min(pos, g_cItems - g_cyPage);
        return pos;
    }
    

    I am not entirely happy with this code, however. It is my personal opinion that the WM_NCCALCSIZE handler should be stateless. Notice that this one modifies state (by calling ScrollTo). If I had more time (sorry, I'm rushed now - you'll learn why soon), I would have put the state modification into the WM_WINDOWPOSCHANGING message. So I will leave that as another exercise.

    Exercise: Keep the WM_NCCALCSIZE message stateless.
  • The Old New Thing

    Why does Win32 fail a module load if an import could not be resolved?

    • 6 Comments

    Because we tried it the other way and it was much worse.

    In 16-bit Windows, a module that didn't satisfy all its imports would still load. As long as you didn't call a missing import, you were fine. If you did try to call a missing import, you crashed pretty spectacularly with the dreaded Unrecoverable Application Error dialog.

    The Win32 folks decided that this was a bad design, because often people would take Fred App, designed for Windows 3.1, and run it on Windows 3.0, and it would run great for about an hour, at which point Fred App would call a function that was available only in Windows 3.1 (like, say, GetSaveFileName) and crash as a result.

    So the Win32 folks decided that if an import could not be resolved, the app should fail loading. If the makers of Fred App wanted to run on Windows 3.0 after all, they could indicate this by using GetProcAddress explicitly. Because if you have to call GetProcAddress explicitly, it'll probably occur to you to check the return value.

    This issue comes up occasionally when people wish out loud, "Gosh, there should be a way I could mark an import as 'optional' - if it couldn't bind, the load should not fail. It would be the app's responsibility to verify that the bind succeeded before calling it." These people are unwittingly asking for history to repeat itself.

  • The Old New Thing

    Eric's complete guide to BSTR semantics

    • 11 Comments

    Eric Lippert has posted Eric's Complete Guide to BSTR Semantics. This is a document that is widely valued within Microsoft, since BSTR semantics are rather tricky, and I'm glad he's willing to share it with the world.

    In particular, Eric spends quite a bit of time discussion the very important equivalence of NULL and L"" in BSTR semantics, something that is buried in paragraph seven of an MSDN document you probably didn't even bother reading.

    If you ask nicely, you might be able to convince him to post his "Complete history of OLEDATE".
  • The Old New Thing

    Scrollbars part 11: Towards an even deeper understanding of the WM_NCCALCSIZE message

    • 6 Comments

    The other form of the WM_NCCALCSIZE message is the complicated one, when the WPARAM is TRUE. In this case, the LPARAM is a pointer to a NCCALCSIZE_PARAMS structure. When Windows sends the WM_NCCALCSIZE message, the NCCALCSIZE_PARAMS structure is filled out like this:

    • rgrc[0] = new window rectangle (in parent coordinates)
    • rgrc[1] = old window rectangle (in parent coordinates)
    • rgrc[2] = old client rectangle (in parent coordinates)

    Notice that the client rectangle is given in parent coordinates, not in client coordinates.

    When your window procedure returns, Windows expects the NCCALCSIZE_PARAMS structure to be filled out like this:

    • rgrc[0] = new client rectangle (in parent coordinates)

    The new client rectangle specifies where the client area of the window should be located, given the new window rectangle.

    Furthermore, if you return anything other than 0, Windows expects the remaining two rectangles to be filled out like this:

    • rgrc[1] = destination rectangle (in parent coordinates)
    • rgrc[2] = source rectangle (in parent coordinates)

    (If you return 0, then Windows assumes that the destination rectangle equals the new client rectangle and the source rectangle equals the old client rectangle.)

    The source and destination rectangles specify which part of the old window corresponds to which part of the new window. Windows will copy the pixels from the source rectangle to the destination rectangle and preserve their validity. The return value of the WM_NCCALCSIZE message specifies how the bits should be matched up if the two rectangles are not the same size. The default behavior is to align them at the top and left edges.

    Let's demonstrate custom valid rectangles with a fresh scratch program. (We'll come back to the scrollbar program.) First, a helper function that computers the "center" of a rectangle.

    void GetRectCenter(LPCRECT prc, POINT *ppt)
    {
        ppt->x = prc->left + (prc->right - prc->left)/2;
        ppt->y = prc->top + (prc->bottom - prc->top)/2;
    }
    

    Exercise: Why do we use the formula c = a + (b-a)/2 instead of the simpler c = (a+b)/2?

    Here's our new PaintContent function:

    void
    PaintContent(HWND hwnd, PAINTSTRUCT *pps)
    {
        //  For debugging flicker - fill with an annoying color for 1 second
        DWORD dwLimit = GdiSetBatchLimit(1);
        FillRect(pps->hdc, &pps->rcPaint,
                 GetSysColorBrush(COLOR_HIGHLIGHT));
        Sleep(1000);
        FillRect(pps->hdc, &pps->rcPaint,
                 GetSysColorBrush(COLOR_WINDOW));
        GdiSetBatchLimit(dwLimit);
    
        //  Draw "concentric" rectangles
        RECT rc;
        GetClientRect(hwnd, &rc);
        POINT ptCenter;
        GetRectCenter(&rc, &ptCenter);
        int limit = max(rc.bottom, rc.right) / 2;
        rc.left = rc.right = ptCenter.x;
        rc.top = rc.bottom = ptCenter.y;
        for (int i = 0; i < limit; i += 10) {
            InflateRect(&rc, 10, 10);
            FrameRect(pps->hdc, &rc, GetSysColorBrush(COLOR_WINDOWTEXT));
        }
    }
    

    When debugging flicker problems, it helps to insert intentionally ugly background painting and annoying pauses so you can see what you are painting. Note, though, that when you do this, you also need to call GdiSetBatchLimit to disable batching. Otherwise, GDI will optimize out the redundant fill and you won't see anything special.

    The real work happens inside our WM_NCCALCSIZE handler.

    UINT OnNcCalcSize(HWND hwnd, BOOL fCalcValidRects,
                               NCCALCSIZE_PARAMS *pcsp)
    {
        UINT uRc = (UINT)FORWARD_WM_NCCALCSIZE(hwnd, fCalcValidRects,
                                               pcsp, DefWindowProc);
    
        if (fCalcValidRects) {
            //  Give names to these things
            RECT *prcClientNew = &pcsp->rgrc[0];
            RECT *prcValidDst  = &pcsp->rgrc[1];
            RECT *prcValidSrc  = &pcsp->rgrc[2];
    
            // Compute the old and new center points
            POINT ptOldCenter, ptNewCenter;
            GetRectCenter(prcValidSrc, &ptOldCenter);
            GetRectCenter(prcClientNew, &ptNewCenter);
    
            //  Tell USER how the old and new client rectangles match up
            *prcValidDst = *prcClientNew; // use entire client area
            prcValidDst->left += ptNewCenter.x - ptOldCenter.x;
            prcValidDst->top += ptNewCenter.y - ptOldCenter.y;
    
            uRc = WVR_VALIDRECTS;
        }
        return uRc;
    }
    
        /* Add to WndProc */
        HANDLE_MSG(hwnd, WM_NCCALCSIZE, OnNcCalcSize);
    

    How does this work?

    If fCalcValidRects, then we do extra work to compute our valid rectangles by seeing how much the window content needs to be shifted and shifting the valid destination rectangle by the same amount. USER copies the upper left corner of the valid source rectangle to the upper left corner of the destination rectangle, so shfiting the upper left corner of the destination rectangle lets us adjust where USER will copy the pixels.

    Play with this program: Grab the window and resize it. Observe that the central portion of the window client area is copied from the original window and is not redrawn. This has two benefits: First, there is no flicker. Second, this improves redraw performance since you aren't drawing pixels unnecessarily. This second remark is particularly important when using the Remote Desktop feature, since Remote Desktop has to transfer all drawn pixels over the network to the client. The fewer pixels you have to transfer, the more responsive your program will be.

    Now that we have a better understanding of the WM_NCCALCSIZE message, we can use this knowledge to improve our scrollbars.

  • The Old New Thing

    Something about Microsoft employees and milk

    • 1 Comments
    Behold the power of milk. Or something. Here's another milk-related bug report from Paul Vick. Paul also has a bug report that challenges the philosophical underpinnings of the eventing model.
  • The Old New Thing

    I'm not sure if this was a dare

    • 6 Comments

    Joe Beda stopped by my office, clutching a stack of documents he had just had notarized, and told me, "Raymond, you should become a notary public."

    "Why?"

    "It'd be fun."

    "Notarizing documents is fun?"

    "No, just being a notary public - just knowing that you had the power to notarize documents - would be fun. Besides, you'd be good at it, since you're such a stickler for the rules. I wonder what the qualifications for being a notary public are, anyway."

    So I went and looked up the process of becoming a notary public in the State of Washington.

    The requirements are deceptively simple: Ability to execute the duties of a notary public (and indeed, requirement to perform said duties upon demand), a $10,000 surety bond, a clean criminal record, and three character references who will affirm that I am "a person of good moral character". That last one might be tricky.
  • The Old New Thing

    Answers to exercises

    • 0 Comments

    Exercise: Explain why we used 0x7FFF to represent infinite height.

    Answer: Commenter "Reiko" got this right. 0x7FFF is the maximum integer coordinate supported by Windows 95, 98 and Me.

    Exercise: Explain the line rcWindow.bottom += rcTemp.top.

    Answer: The more precise way of writing the line would have been

        rcWindow.bottom += (rcTemp.top - rcWindow.top) - (0 - rcWindow.top);
    
    The first term is the amount of non-client space consumed at the top of the window. The second term is the amount of non-client space consumed at the top of the window, taking wrapping into account. The difference, therefore is the amount by which AdjustWindowRectEx needs to be adjusted. But the two instances of rcWindow.top cancel out, leaving just rcTemp.top.
  • The Old New Thing

    Whimsical bug reports

    • 12 Comments

    Whimsical bug reports, while not a common occurence, aren't exactly unheard of either. They are a popular way to vent a shared frustration and lighten the mood.

    Recently, we changed milk suppliers for our cafeterias. Well, more accurately, our previous milk supplier was bought by another milk company. The problem is that the single-serving milk cartons from the new company are hard to open.

    So of course what you do is file a bug.

    Bug: New milk cartons are hard to open.

    Repro: Go to cafeteria, get milk carton, attempt to open it, get napkins and clean up mess.

    (The reason is that the milk company bought a brand new machine which seals the cartons with too much glue. They are working to adjust the seal.)

    A few work-arounds were suggested, including bringing your own cow and a three-step process of freezing the milk, tearing the carton open, then allowing the milk to thaw. Others explain that the fix is held up in testing: "Currently only 3 testers are handling this component and they can only drink 8 cartons a day. The team could conduct more carton-opening tests but carton-tasting, milkflow testing and carton pressure tests are still remaining." Plus of course a security review needs to be made of the consequences of a weaker seal.

    This is a particularly software-oriented joke, because it highlights how hard it is to make bugfixes in software - by applying the software testing regimen to something that isn't software. You can't assume that a simple, local change like adjusting the amount of glue applied to the carton will result in a simple, local change in the final product (a more acceptable seal strength). Software is nonlinear. A simple change can have effects (some catastrophic, some subtle) far, far away from the point of change.

  • The Old New Thing

    Why does the taskbar default to the bottom of the screen?

    • 13 Comments

    It didn't always.

    The original taskbar didn't look at all like what you see today. It defaulted to the top of the screen and looked something like this.

    This is definitely not what it actually looked like. It has been so long I forgot precisely what it looked like (I didn't realize there was going to be a quiz ten years later), but this captures the basic flavor, at least for the purpose of this discussion.

    The point is that the bar took the form, not of buttons, but of tabs. Each tab corresponded to a running window, which melded into the tab. You switched window by clicking the corresponding tab.

    You can see vestiges of this style in the TCS_BUTTONS style in the tab control. When we switched to the button-look for the taskbar, we still had a lot of switching code based on the tabs metaphor, and it was less work to add a button-look to the tab control than it was to rewrite all the switching code.

    The tabbed look was abandoned for various reasons, one of which was what everybody else has already noticed: If you put the taskbar at the top of the screen, lots of windows end up sliding under it, because they assumed that the usable area of the screen began at (0,0). Other windows would "creep" up the screen because they used GetWindowPlacement to save their window position (which returns workspace coordinates, where (0,0) is the first usable pixel) but use SetWindowPos to restore it (which uses screen coordinates, where (0,0) is the upper left pixel of the primary monitor).

    There were too many apps that kept sliding under the top-docked taskbar so we had to abandon that idea and move it to the bottom.

    It's somewhat disheartening to observe that now, eight years later, apps still mess up their coordinate systems and keep sliding under a top-docked or left-docked taskbar.

Page 2 of 4 (38 items) 1234