Scrollbars part 12: Applying WM_NCCALCSIZE to our scrollbar sample


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.

Comments (2)
  1. Adrian says:

    ClampScrollPos() has a bug. (I recently made the same mistake in one of my apps.) If the window is large enough that the window doesn’t need a scroll bar, then ClampScrollPos will return a negative value (because g_cItems – g_cyPage will be less than zero). The simple fix is to swap the two lines.

    In my apps, I’ve always tracked scrolling by pixels rather than lines. It simplifies many of the calculations, and smooth scrolling becomes a possibility. You just need a multiplier in the scroll-by-line functions that tells you the (average) pixels per line.

    Great series, though. I thought I knew everything there was about getting scrolling just right, but I’ve already learned a couple new bits. In a sense it’s a shame that it’s this much work to get scrolling right. That could explain all the custom controls out there that don’t do it right.

    ANIMATED SCROLLING

    I like animated scrolling to help the eye follow along. In addition to keeping track of the current scroll position, you also track the current *target* position. All of your scroll commands then compute a new target position.

    I make it a user selectable option (using SPI_GETLISTBOXSMOOTHSCROLLING as a default). If animated scrolling is disabled, then iPos = iTarget and proceed as usual.

    But if it’s enabled, send yourself a custom "animate" message instead. The handler for this message moves the current position closer to the target position and calls ScrollWindow(). At the end of OnPaint, you post another "animate" message if the current position hasn’t caught up with the target. Since WM_PAINT messages are low priority (only dispatched when cue is empty), you can still accumulate additional scrolling commands from the mouse or keyboard and move the target as the animation proceeds.

    What we want is snappy animation (so you can still jump to the top or bottom of a long document quickly) while giving enough motion for the eye to track and land. So what I do is:

    int delta = iTarget – iPos;

    if (-3 < delta || delta < 3)

    iPos += delta / 3;

    else

    iPos = iTarget

    Thus, if you have a long way to scroll, we whizz by. But as we get to the target location, the rate slows nicely. Really slick if you track things in pixels rather than items.

  2. Adrian says:

    Oops, I got the comparison wrong at the end of my last post. Should be:

    if (-3 < delta && delta < 3)

    iPos = iTarget;

    else

    iPos += delta/3;

Comments are closed.