我该如何处理 WM_MOUSEWHEEL?

How do I handle WM_MOUSEWHEEL?

我正在处理如下所示的 WM_MESSAGE,但我根本无法移动 window。我不知道为什么。我错过了什么?

void handleMouseWheel(HWND hwnd, int delta)
{
    SCROLLINFO si;
    si.cbSize = sizeof(SCROLLINFO);
    si.fMask = SIF_PAGE | SIF_POS;

    if(!GetScrollInfo(hwnd, SB_VERT, &si)) {
        ErrorExit(NAMEOF(GetScrollInfo),  __LINE__, __FILENAME__);   
    }

    int nOldPos = si.nPos;
    int nPos = nOldPos + wheelScrollLines(hwnd, delta, si.nPage);
    
    SetLastError(0);
    nPos = SetScrollPos(hwnd, SB_VERT, nPos, TRUE);

    POINT pt;
    pt.x = 0;
    pt.y = nOldPos - nPos;

    HDC hdc = GetDC(hwnd);
    if(!hdc) {
        assert(!"GetDC failed");
    }

    if(!LPtoDP(hdc, &pt, 1)) {
        assert(!"LPtoDP failed");
    }

    ReleaseDC(hwnd, hdc);

    if(!ScrollWindow(hwnd, 0, -pt.y, NULL, NULL)) {
        ErrorExit(NAMEOF(ScrollWindow), __LINE__, __FILENAME__);
    }
}

其中 wheelScrollLines() 定义为:

int wheelScrollLines(HWND hwnd, int delta, int nPage)
{
    static int accumulator;
    static int lastActivity;
    static HWND lastHwnd;

    int lines;

    int dwNow = GetTickCount();

    if(nPage < 1) {
        nPage = 1;
    }

    int linesPerDataWheelDelta = defScrollSpeed;
    SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &linesPerDataWheelDelta, 0);

    if(linesPerDataWheelDelta == WHEEL_PAGESCROLL || linesPerDataWheelDelta > nPage) {
        linesPerDataWheelDelta = nPage;
    }

    if(lastHwnd != hwnd)
    {
        lastHwnd = hwnd;
        accumulator = 0;
    }
    else if((dwNow - lastActivity) > GetDoubleClickTime() * 2)
    {
        accumulator = 0;
    }
    else if(accumulator > 0 && delta < 0)
    {
        accumulator = 0;
    }

    if(linesPerDataWheelDelta > 0)
    {
        accumulator += delta;
        lines = (accumulator * linesPerDataWheelDelta) / WHEEL_DELTA;
        accumulator -= (lines * WHEEL_DELTA) / linesPerDataWheelDelta;
    }
    else
    {
        lines = 0;
        accumulator = 0;
    }

    lastActivity = dwNow;

    return -lines;
}

完整代码:

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "Comctl32.lib")
#pragma comment(lib, "Gdi32.lib")

#define WIN32_LEAN_AND_MEAN
#define UNICODE
#define _UNICODE

#include <windows.h>
#include <limits.h>
#include <Commctrl.h>
#include <crtdbg.h>
#include <strsafe.h>
#include <string.h>
#include <assert.h>

#ifdef UNICODE
#define STRSPLIT wcsrchr
#else
#define STRSPLIT strrchr
#endif

#define __FILENAME__ (STRSPLIT(TEXT(__FILE__), '/') ? STRSPLIT(TEXT(__FILE__), '/') + 1 : TEXT(__FILE__))
#define NAMEOF(s) TEXT(#s)
#define COUNTOF(a) (sizeof(a)/sizeof(a[0]))

#define defScrollSpeed 3

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK WndProc1(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WndProc2(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
void createWindow1(HWND);
void createWindow2(HWND);
int scrollHeight(void);
DWORD ShowLastError(LPWSTR lpszFunction, int line, LPWSTR filename);
void ErrorExit(LPWSTR lpszFunction, int line, LPWSTR filename);
int scrollHeight(void);
int getHeight(HWND control);
void setUpScrollBar(HWND hwnd);
void handleMouseWheel(HWND hwnd, int delta);
int wheelScrollLines(HWND hwnd, int delta, int nPage);

HBRUSH hBrush1, hBrush2;

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PWSTR lpCmdLine, int nCmdShow) {

    MSG  msg;    
    WNDCLASSW wc = {0};
    wc.lpszClassName = L"my window";
    wc.hInstance     = hInstance;
    wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE);
    wc.lpfnWndProc   = WndProc;
    wc.hCursor       = LoadCursor(0, IDC_ARROW);

    hBrush1 = CreateSolidBrush(RGB(173, 164, 237));
    hBrush2 = CreateSolidBrush(RGB(171, 171, 171));

    RegisterClassW(&wc);
    HWND hWnd =
    CreateWindowW(wc.lpszClassName, L"window",
                  WS_OVERLAPPEDWINDOW | WS_VISIBLE | ES_AUTOHSCROLL,
                  100, 100, 330, 270, NULL, 0, hInstance, 0);

    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (!IsDialogMessage(hWnd, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    DeleteObject(hBrush1);
    DeleteObject(hBrush2);

    return (int) msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, 
    WPARAM wParam, LPARAM lParam) {

    switch(msg)
    {
        case WM_CREATE:
            createWindow1(hwnd);
            createWindow2(hwnd);
            setUpScrollBar(hwnd);
        break;

        case WM_MOUSEWHEEL:
            handleMouseWheel(hwnd, HIWORD(wParam));
            return 0;

        case WM_DESTROY:
            PostQuitMessage(0);
            break;
    }

    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

void createWindow1(HWND hOwner)
{
    WNDCLASSW wc = {0};
    wc.lpszClassName = L"window2";
    wc.hInstance     = NULL;
    wc.hbrBackground = hBrush1;
    wc.lpfnWndProc   = WndProc1;
    wc.hCursor       = LoadCursor(0, IDC_ARROW);

    RegisterClassW(&wc);
    CreateWindowW(wc.lpszClassName, L"window2",
                  WS_VISIBLE | WS_TABSTOP | WS_CHILD,
                  5, 5, 300, 200, 
                  hOwner, 0, NULL, 0);
}

void createWindow2(HWND hOwner)
{
    WNDCLASSW wc = {0};
    wc.lpszClassName = L"window3";
    wc.hInstance     = NULL;
    wc.hbrBackground = hBrush2;
    wc.lpfnWndProc   = WndProc2;
    wc.hCursor       = LoadCursor(0, IDC_ARROW);

    RegisterClassW(&wc);
    CreateWindowW(wc.lpszClassName, L"window3",
                  WS_VISIBLE | WS_TABSTOP | WS_CHILD,
                  5, 120, 300, 200, 
                  hOwner, 0, NULL, 0);
}

LRESULT CALLBACK WndProc1(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{

    switch(msg)
    {
        case WM_CREATE:
            CreateWindow(L"Button", L"Button A",
                         WS_VISIBLE | WS_TABSTOP | WS_CHILD,
                         5, 5, 80, 25,
                         hwnd, 0, NULL, 0);
        break;

        case WM_KEYUP:
        case WM_KEYDOWN:
            MessageBox(NULL, L"hello from proc1", L"", MB_OK);
            break;

        case WM_DESTROY:
            PostQuitMessage(0);
            break;
    }

    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

LRESULT CALLBACK WndProc2(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 
{

    switch(msg)
    {
        case WM_CREATE:
            CreateWindow(L"Button", L"Button B", 
                         WS_VISIBLE | WS_TABSTOP | WS_CHILD,
                         5, 5, 80, 25,
                         hwnd, 0, NULL, 0);
        break;

        case WM_DESTROY:
            PostQuitMessage(0);
            break;
    }

    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

void setUpScrollBar(HWND hwnd)
{
    RECT rc = { 0 };
    GetClientRect(hwnd, &rc);
    SCROLLINFO si = { 0 };
    si.cbSize = sizeof(SCROLLINFO);
    si.fMask = SIF_ALL;
    si.nMin = 0;
    si.nMax = 300;
    si.nPage = (rc.bottom - rc.top);
    si.nPos = 0;
    si.nTrackPos = 0;
    SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
}

int wheelScrollLines(HWND hwnd, int delta, int nPage)
{
    static int accumulator;
    static int lastActivity;
    static HWND lastHwnd;

    int lines;

    int dwNow = GetTickCount();

    if(nPage < 1) {
        nPage = 1;
    }

    int linesPerDataWheelDelta = defScrollSpeed;
    SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &linesPerDataWheelDelta, 0);

    if(linesPerDataWheelDelta == WHEEL_PAGESCROLL || linesPerDataWheelDelta > nPage) {
        linesPerDataWheelDelta = nPage;
    }

    if(lastHwnd != hwnd)
    {
        lastHwnd = hwnd;
        accumulator = 0;
    }
    else if((dwNow - lastActivity) > GetDoubleClickTime() * 2)
    {
        accumulator = 0;
    }
    else if(accumulator > 0 && delta < 0)
    {
        accumulator = 0;
    }

    if(linesPerDataWheelDelta > 0)
    {
        accumulator += delta;
        lines = (accumulator * linesPerDataWheelDelta) / WHEEL_DELTA;
        accumulator -= (lines * WHEEL_DELTA) / linesPerDataWheelDelta;
    }
    else
    {
        lines = 0;
        accumulator = 0;
    }

    lastActivity = dwNow;

    return -lines;
}

void handleMouseWheel(HWND hwnd, int delta)
{
    SCROLLINFO si;
    si.cbSize = sizeof(SCROLLINFO);
    si.fMask = SIF_PAGE | SIF_POS;

    if(!GetScrollInfo(hwnd, SB_VERT, &si)) {
        ErrorExit(NAMEOF(GetScrollInfo),  __LINE__, __FILENAME__);   
    }

    int nOldPos = si.nPos;
    int nPos = nOldPos + wheelScrollLines(hwnd, delta, si.nPage);
    
    SetLastError(0);
    nPos = SetScrollPos(hwnd, SB_VERT, nPos, TRUE);

    POINT pt;
    pt.x = 0;
    pt.y = nOldPos - nPos;

    HDC hdc = GetDC(hwnd);
    if(!hdc) {
        assert(!"GetDC failed");
    }

    if(!LPtoDP(hdc, &pt, 1)) {
        assert(!"LPtoDP failed");
    }

    ReleaseDC(hwnd, hdc);

    if(!ScrollWindow(hwnd, 0, -pt.y, NULL, NULL)) {
        ErrorExit(NAMEOF(ScrollWindow), __LINE__, __FILENAME__);
    }
}

int getHeight(HWND control)
{
    RECT rt;

    if(!GetWindowRect(control, &rt)) {
        ErrorExit(NAMEOF(getHeight), __LINE__, __FILENAME__);
    }

    return rt.bottom - rt.top;
}

void ErrorExit(LPWSTR lpszFunction, int line, LPWSTR filename)
{
    DWORD dw = ShowLastError(lpszFunction, line, filename);
    ExitProcess(dw);
}

DWORD ShowLastError(LPWSTR lpszFunction, int line, LPWSTR filename)
{
    #define MAX_DIGITS 16

   /* 
    * NOTE!!: calling GetLastError() must be done before calling
    * any other function, that would reset the GetLastError(), making
    * this function report error about the wrong function.
    */
    DWORD dw = GetLastError();
    LPVOID lpMsgBuf;
    LPVOID lpDisplayBuf;
    
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        dw,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR) &lpMsgBuf,
        0,
        NULL
    );

    lpDisplayBuf = (LPVOID) LocalAlloc(LMEM_ZEROINIT, 
            (lstrlen((LPCTSTR)lpMsgBuf) +
            lstrlen((LPCTSTR)lpszFunction) + 40 +
            (line > 0 ? MAX_DIGITS : 0) +
            (filename != NULL ? lstrlen(filename) : 0)) *
            sizeof(TCHAR)
    );
    StringCchPrintf((LPTSTR)lpDisplayBuf,
                    LocalSize(lpDisplayBuf) / sizeof(TCHAR),
                    TEXT("%s failed with %d: %s"),
                    lpszFunction, dw, lpMsgBuf
    );
    MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);
    LocalFree(lpMsgBuf);
    LocalFree(lpDisplayBuf);
    return dw;
}

您正在使用 HIWORDWM_MOUSEWHEEL 消息中的 wParam 中提取增量,但根据 documentation 这是不正确的:

HIWORD returns 一个无符号值;由于增量是有符号值,因此您需要改用 GET_WHEEL_DELTA_WPARAM

正如乔纳森所说,您需要使用:

handleMouseWheel(hwnd, GET_WHEEL_DELTA_WPARAM(wParam));

但是 window 没有移动,因为计算错误,而不是缺少 SW_SCROLLCHILDREN 标志。当你打电话时:

nPos = SetScrollPos(hwnd, SB_VERT, nPos, TRUE);

npos会更新到滚动框的前一个位置。此时npos等于nOldPos。并且每次后续更新都将相等。

然而,pt.y = nOldPos-nPos;。所以 pt.y 每次都是 0。当你调用ScrollWindow(hwnd, 0, pt.y, NULL, NULL)时,window不会移动。

因此您需要将代码更改为:

//nPos = SetScrollPos(hwnd, SB_VERT, nPos, TRUE); //old code
SetScrollPos(hwnd, SB_VERT, nPos, TRUE);
...
if (!ScrollWindow(hwnd, 0, pt.y, NULL, NULL))
...

编辑:

因为你的pt.y = nOldPos-nPos;可能会让pt.y在顶部计算出错误的值或者bottom.So你可以直接使用GetScrollInfo,像这样:

void handleMouseWheel(HWND hwnd, int delta)
{
    SCROLLINFO si;
    si.cbSize = sizeof(SCROLLINFO);
    si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;

    if (!GetScrollInfo(hwnd, SB_VERT, &si)) {
        ErrorExit(LPWSTR(NAMEOF(GetScrollInfo)), __LINE__, LPWSTR(__FILENAME__));
    }
    int nOldPos = si.nPos;
    int nPos = nOldPos + wheelScrollLines(hwnd, delta, si.nPage);
    si.fMask = SIF_POS;
    SetScrollPos(hwnd, SB_VERT, nPos, TRUE);
    GetScrollInfo(hwnd, SB_VERT, &si);

    if (!ScrollWindow(hwnd, 0, nOldPos - si.nPos, NULL, NULL)) {
        ErrorExit(LPWSTR(NAMEOF(ScrollWindow)), __LINE__, LPWSTR(__FILENAME__));
    }
    UpdateWindow(hwnd);
}

它对我有用。