Scrollbars bart 6 – The wheel


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?

Comments (4)
  1. milbertus says:

    #1: Isn’t that because WHEEL_DELTA might be a float or double, which would cause a truncation (or cast) compiler warning?

    #2: Hmmm…maybe you could hard code in a huge value, so that one click of your mouse wheel wouldn’t accumulate to the amount of what your app is looking for.

  2. MacTruck says:

    Those who can’t do math right should not write code. Period.

    For those who are looking to use the above to calculate delta carryovers, the above formula for is mathematically wrong. Let’s go through code with the following assumptions:

    uScroll = 3;

    zDelta = WHEEL_DELTA / 2;

    g_iWheelCarryover = 0;

    Those are the initial default values the system should give. WHEEL_DELTA, according to MSDN Library, is 120, so zDelta = 60;

    zDelta += g_iWheelCarryover;

    // zDelta += 0;

    // Therefore, zDelta is still 60.

    int dLines = zDelta * (int)uScroll / WHEEL_DELTA;

    // dLines = 60 * 3 / 120;

    // Therefore, dLines = 1. 180 / 120 = 1.5, but this is truncated to 1 because it is an integer.

    g_iWheelCarryover = zDelta – (dLines * WHEEL_DELTA / (int)uScroll);

    // g_iWheelCarryover = 60 – (1 * 120 / 3);

    // Therefore, g_iWheelCarryover = 60 – 40 = 20.

    20 is incorrect and that algorithm is _WAY_ off base. 60 is what the value _SHOULD_ be. g_iWheelCarryover is supposed to contain the remainder of the scroll amount in WHEEL_DELTA units. In this case, 1/2 a line (.5) was "lost". The correct algorithm for g_iWheelCarryover should be:

    g_iWheelCarryover = (zDelta * (int)uScroll) – (dLines * WHEEL_DELTA);

    // g_iWheelCarryover = (60 * 3) – (1 * 120);

    // g_iWheelCarryover = (180) – (120);

    // g_iWheelCarryover = 60;

    This also means that the check for divide by zero is unnecessary.

    Learn to write code correctly before posting to the Internet.

    (Oh, and the answer to #2 is to use SendMessage() with the appropriate parameters).

  3. Raymond Chen says:

    Um, no, 20 is the correct value for g_iWheelCarryover at the end. 40 units of zDelta were used to scroll one line, leaving 20 units of carryover.

    Learn to write code correctly before posting to the Internet.

Comments are closed.

Skip to main content