August, 2003

  • The Old New Thing

    Scrollbars part 7 - Integrality

    • 0 Comments

    If you play around with resizing the window, you can get a fractional line to appear at the bottom of the screen. This is not normally a problem until you scroll to the very end of the list, say, by pressing the End key, at which point an ugly blank space appears at the bottom. This ugly blank space is particularly disturbing when the fractional line is very nearly an entire line, because it looks like there is an off-by-one bug in the code somewhere.

    We can fix this by forcing the window size to be an exact integral multiple of the line height. Like adding scrollbars, there is the basic idea, followed by a lot of detail work to get it just right.

    The basic idea is to enforce integrality in the window resize code. The right place to do this is in the WM_WINDOWPOSCHANGING handler, which allows you to adjust the placement of the window before it is actually moved. This avoids flicker.

    We'll break the bulk of the work into a helper function, which will prove useful later.

    void AdjustSizeRectangle(HWND hwnd, WPARAM wmsz, LPRECT prc)
    {
        RECT rc;
        int cyClient;
        int cyAdjust;
    
        /* Compute the resulting client height */
        SetRectEmpty(&rc);
        AdjustWindowRect(&rc, GetWindowStyle(hwnd), FALSE);
        cyClient = (prc->bottom - prc->top) - (rc.bottom - rc.top);
    
        /* Compute the number of fractional pixels */
        cyAdjust = cyClient % g_cyLine;
    
        /*
         *  Remove the fractional pixels from the top or bottom.
         */
        switch (wmsz) {
        case WMSZ_TOP:
        case WMSZ_TOPLEFT:
        case WMSZ_TOPRIGHT:
            prc->top += cyAdjust;
            break;
        default:
            prc->bottom -= cyAdjust;
            break;
        }
    }
    

    The WM_WINDOWPOSCHANGNG handler then check if the window size is changing, in which case we adjust the size rectangle to enforce integrality of the client area. We say that the adjustments should be taken from the bottom of the window.

    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;
    }
    
        /* Add to WndProc */
        HANDLE_MSG(hwnd, WM_WINDOWPOSCHANGING, OnWindowPosChanging);
    
  • The Old New Thing

    Answers to exercises

    • 0 Comments

    What is the significance of the (int) cast in the computation of dLines?

    It isn't. I got the question wrong. The real question should have been "What is the significance of the (int) cast in the computation of g_iWheelCarryover?"

    The answer is to ensure that the computation is performed with signed integers throughout. If the cast were missing, then the computation would have been unsigned (since mixing signed and unsigned yields unsigned). dLines is a signed integer, let's say it's -1. Multiply by WHEEL_DELTA yields -120. Now convert it to an unsigned integer and you get a number in excess of four billion. Divided by uScroll (typically 3) yields a number greater than one billion, which is incorrect.

    Assuming you don't have a high-resolution wheel mouse, how would you test that your sub-detent mouse wheel handling was working properly?

    This is an easy one. Insert temporary code at the top of the OnMouseWheel function that says zDelta /= 4. Ta-da, your mouse now has quadruple its original resolution.

  • The Old New Thing

    Why is a registry file called a "hive"?

    • 9 Comments

    Useless trivia day.

    Why is a registry file called a "hive"?

    Because one of the original developers of Windows NT hated bees.  So the developer who was responsible for the registry snuck in as many bee references as he could.  A registry file is called a "hive", and registry data are stored in "cells", which is what honeycombs are made of.

  • The Old New Thing

    A subtlety in the keyboard code

    • 0 Comments
    I neglected to call it out explicitly in the text for Part 5:  Notice that I use cRepeat to determine how many lines/pages to scroll.  This allows us to scroll the correct amount even if we are falling behind on input processing and are missing some autorepeats.
  • The Old New Thing

    Scrollbars bart 6 - The wheel

    • 4 Comments

    The mouse wheel is tricky because the mouse wheel UI guidelines indicate that you should scroll by a user-specified amount for each "click" of the mouse, where one click is WHEEL_DELTA mouse units (called a "detent"). There are two subtle points about the above requirement: First, that the amount of scrolling is a user setting which must be respected, and second, that the wheel can report values that are not perfect multiples of WHEEL_DELTA.

    In particular, there is the possibility that a high-resolution mouse wheel will report wheel scroll units smaller than WHEEL_DELTA. For example, consider a wheel mouse that supports "half-clicks". When you turn the wheel halfway between clicks, it reports WHEEL_DELTA/2, and when you reach a full click, it reports another WHEEL_DELTA/2. To handle this properly, you need to make sure that by the time the full click is reached, the window has scrolled by exactly the amount it would have scrolled if the user had been using a low-resolution wheel that reported a single wheel motion of WHEEL_DELTA.

    (I once referred to this in email as a "sub-detent wheel" and was accused of coining a phrase.)

    To handle the first point, we requery the user's desired scroll delta at each mouse wheel message. To handle the second point, we accumulate detents as they arrive and consume as many of them as possible, leaving the extras for the next wheel message.

    int g_iWheelCarryover;      /* Unused wheel ticks */
    
    LRESULT OnMouseWheel(HWND hwnd, int xPos, int yPos, int zDelta, UINT fwKeys)
    {
        UINT uScroll;
        if (!SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &uScroll, 0)) {
            uScroll = 3;    /* default value */
        }
    
        /*
         *  If user specified scrolling by pages, do so.
         */
        if (uScroll == WHEEL_PAGESCROLL)
        {
            uScroll = g_cLinesPerPage;
        }
    
        /*
         *  If user specified no wheel scrolling, then don't do wheel scrolling.
         *  This also avoids a divide-by-zero below.
         */
        if (uScroll == 0)
        {
            return 0;
        }
    
        zDelta += g_iWheelCarryover;    /* Accumulate wheel motion */
    
        /*
         *  See how many lines we should scroll.
         *  This relies on round-towards-zero.
         */
        int dLines = zDelta * (int)uScroll / WHEEL_DELTA;
    
        /*
         *  Record the unused portion as the next carryover.
         */
        g_iWheelCarryover = zDelta - dLines * WHEEL_DELTA / (int)uScroll;
    
    
        /*
         *  Do the scrolling.
         */
        ScrollDelta(hwnd, -dLines);
    
        return 0;
    }
    
        /* Add to WndProc */
        HANDLE_MSG(hwnd, WM_MOUSEWHEEL, OnMouseWheel);
    

    Exercise: What is the significance of the (int) cast in the computation of dLines?

    Exercise: Assuming you don't have a high-resolution wheel mouse, how would you test that your sub-detent mouse wheel handling was working properly?

  • The Old New Thing

    Limitations on DLL resources in Windows 95

    • 1 Comments

    Ancient history lesson.

    When Win9x loads a 32-bit DLL, it creates a shadow 16-bit DLL so 16-bit code (like USER) can access resources in it.

    The shadow DLL is effectively a resource-only 16-bit DLL, created by taking the 32-bit resources and converting them to 16-bit format. If the resources cannot be converted to 16-bit format, the DLL will not load.

    The 16-bit resource file format specifies resource sizes by combining a DLL-wide shift value with a 16-bit per-resource scaled size. So, for example, if the shift value were 2, and the per-resource scaled size were 8, then the actual resource size would be 8 << 2 = 32.

    Windows 95 has a bug in the way it calculates the scaled size.

    If the Windows 95 kernel decided that it needed to use a nonzero shift value because the 32-bit DLL contains a resource larger than 64K, it scaled the 32-bit values down to 16-bit values and rounded down rather than up. So, for example, if a resources were 65537 bytes in size and the shift factor were 1, then the scaled-down value would be 65537 >> 1 = 32768. After scaling back up, the result would be 32768 >> 1 = 65536. Notice that the value is too small; the last byte of the resource has been truncated.

    Consequently, if you have a 32-bit DLL with resources 64K or larger, you must pad those resources to prevent this truncation from happening. In the above example, you would have to pad the resource to 65538 bytes, so that the scaled-down value would be 32769, which scales back up to 65538.

    I believe this bug was fixed in Windows 98 but I'm not sure. There is a little program in the SDK called fixres95 that generates the necessary padding.

    Other limitations of the 16-bit resource file format you may run into:

    • Ordinal resource identifiers may not exceed 32767.
    • The total lengths of named resources may not exceed 65535 (where each name counts one byte for each character in the name, plus one). Named resources have been a bad idea since Windows 1.0. They are a convenience that you can easily live without most of the time, and they are significantly more costly, as you can see.
  • The Old New Thing

    Keyboard accessibility for scrollbars

    • 3 Comments

    Note that so far, the scrollbar is accessible only with the mouse. Our next step is to add keyboard access to the scrollbar. Fortunately, this is not all that difficult. We merely map some keystrokes to equivalent scrollbar actions.

    void OnKey(HWND hwnd, UINT vk, BOOL fDown, int cRepeat, UINT flags)
    {
        if (fDown) {
            switch (vk) {
            case VK_UP:         ScrollDelta(hwnd, -cRepeat); break;
            case VK_DOWN:       ScrollDelta(hwnd, +cRepeat); break;
            case VK_PRIOR:      ScrollDelta(hwnd, -cRepeat*g_cyPage); break;
            case VK_NEXT:       ScrollDelta(hwnd, +cRepeat*g_cyPage); break;
            case VK_HOME:       ScrollTo(hwnd, 0); break;
            case VK_END:        ScrollTo(hwnd, MAXLONG); break;
            }
        }
    }
    
        /* Add to WndProc */
        HANDLE_MSG(hwnd, WM_KEYDOWN, OnKey);
    

    Note that this doesn't make our sample program fully accessible; this just makes the scrollbars accessible. Full accessibility will be covered in a (much) later blog entry. Right now, I'm just focusing on scrollbars.

  • The Old New Thing

    Changing the Windows boot logo

    • 12 Comments

    This is the answer I give to IT people when they ask if it's okay to customize the Windows boot logo.

    DO NOT DO THIS ON A PRODUCTION MACHINE OR YOU WILL REGRET IT.

    If you hack the bitmap everything will seem fine until six months down the road when you decide to install the latest service pack. The service pack installer will not upgrade your ntoskrnl because it looks at the file and says "Hm, this isn't the standard uniprocessor ntoskrnl, it's not the standard multiprocessor ntoskrnl, it's not the standard advanced server ntoskrnl, I can't tell what this is, so I don't know which version of ntoskrnl to update it to. I'll just leave it alone."

    If you are lucky you will bluescreen at boot because the old ntoskrnl is incompatible with some other critical part of the service pack.

    If you are unlucky, your machine will appear to run normally when in fact it is quietly corrupting itself, and then it will keel over or generate bogus data when you least expect it.

    If you planned ahead, you will have quit your job and moved to Hawaii so the disaster falls on your replacement's head to clean up while you sit on the beach sipping a pina colada.

  • The Old New Thing

    Answer to previous exercise

    If you look at the WM_VSCROLL message, you'll see that the scroll position is a 16-bit value. So if the number of entries is more then 65535, you won't be able to use the scroll thumb to get to the ones at the end.

    Try it: Change the value of g_cItems to 100000 and watch what happens.

    The fix is to ignore the pos passed to the message and instead get it from the scrollbar. This helper function will prove useful.

    UINT GetTrackPos(HWND hwnd, int fnBar)
    {
        SCROLLINFO si;
        si.cbSize = sizeof(si);
        si.fMask = SIF_TRACKPOS;
        if (GetScrollInfo(hwnd, fnBar, &si)) {
            return si.nTrackPos;
        }
        return 0;
    }
    

    Change the two case statements in OnVscroll as follows:

        case SB_THUMBPOSITION:  ScrollTo(hwnd, GetScrollPos(hwnd, SB_VERT)); break;
        case SB_THUMBTRACK:     ScrollTo(hwnd, GetTrackPos(hwnd, SB_VERT)); break;
    
Page 3 of 3 (29 items) 123