Search code examples
c++winapimouseeventmouselistenersetwindowshookex

C++ Identifying X Buttons & Scroll Wheel Directions


I have recently been experimenting with a little project during my limited free time to try and gain more experience and understanding with C++, but I've come to a roadblock in my current program:

I'm trying to create a global low-level mouse listener by using a windows hook, which most things seem fairly straight forward. However, identifying which X mouse button was clicked (MB4 or MB5) and which direction the scroll wheel was rolled is giving me a whole lot of headache.

According to the Microsoft docs, the current way I am trying to identify the appropriate X button clicked and scroll wheel direction is correct, but my implementation of it is not working.

I have been able to find one working solution to the X button issue (the last code segment post in this forum thread), but it seems a bit like jumping through unnecessary hoops when the Microsoft code segment is cleaner and should work.

Though C++ is not my most familiar language, I would like to continue to learn it and use it more often. I hope I'm just making a simple mistake, as this is the first time I have been working with Windows hooks. Thank you in advance for any advice or assistance anyone may be able to offer!

#include <iostream>
#include <windows.h>

static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) 
{
    if(nCode >= 0)
    {
        switch(wParam)
        {
            case WM_LBUTTONDOWN:
                system("CLS");
                std::cout << "left mouse button down\n";
                break;
            case WM_LBUTTONUP:
                std::cout << "left mouse button up\n";
                break;
            case WM_RBUTTONDOWN:
                system("CLS");
                std::cout << "right mouse button down\n";
                break;
            case WM_RBUTTONUP:
                std::cout << "right mouse button up\n";
                break;
            case WM_MBUTTONDOWN:
                system("CLS");
                std::cout << "middle mouse button down\n";
                break;
            case WM_MBUTTONUP:
                std::cout << "middle mouse button up\n";
                break;
            case WM_MOUSEWHEEL:
                if(GET_WHEEL_DELTA_WPARAM(wParam) > 0)
                    std::cout << "mouse wheel scrolled up\n";
                else if(GET_WHEEL_DELTA_WPARAM(wParam) < 0)
                    std::cout << "mouse wheel scrolled down\n";
                else //always goes here
                    std::cout << "unknown mouse wheel scroll direction\n";
                break;
            case WM_XBUTTONDOWN:
                system("CLS");
                if(GET_XBUTTON_WPARAM(wParam) == XBUTTON1)
                    std::cout << "X1 mouse button down\n";
                else if(GET_XBUTTON_WPARAM(wParam) == XBUTTON2)
                    std::cout << "X2 mouse button down\n";
                else //always goes here
                    std::cout << "unknown X mouse button down\n";
                break;
            case WM_XBUTTONUP:
                if(GET_XBUTTON_WPARAM(wParam) == XBUTTON1)
                    std::cout << "X1 mouse button up\n";
                else if(GET_XBUTTON_WPARAM(wParam) == XBUTTON2)
                    std::cout << "X2 mouse button up\n";
                else //always goes here
                    std::cout << "unknown X mouse button up\n";
                break;
        }
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

int main()
{
    HHOOK mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, NULL, 0);
    MSG msg;

    while(GetMessage(&msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    UnhookWindowsHookEx(mouseHook);
    return 0;
}

Solution

  • Please read the documentation:

    LowLevelMouseProc callback function:

    [...]

    wParam [in]
    Type: WPARAM
    The identifier of the mouse message. This parameter can be one of the following messages:
    WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_RBUTTONDOWN, or WM_RBUTTONUP.

    lParam [in]
    Type: LPARAM
    A pointer to an MSLLHOOKSTRUCT structure.

    So wParam can be WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_RBUTTONDOWN, or WM_RBUTTONUP. There is no magic way to get any more information out of it. And if there were it would be undocumented and should be avoided.

    lParam however points to a MSLLHOOKSTRUCT:

    tagMSLLHOOKSTRUCT structure:

    Contains information about a low-level mouse input event.

    typedef struct tagMSLLHOOKSTRUCT {
      POINT     pt;
      DWORD     mouseData;
      DWORD     flags;
      DWORD     time;
      ULONG_PTR dwExtraInfo;
    } MSLLHOOKSTRUCT, *LPMSLLHOOKSTRUCT, *PMSLLHOOKSTRUCT;
    

    [...]

    mouseData
    Type: DWORD

    If the message is WM_MOUSEWHEEL, the high-order word of this member is the wheel delta. The low-order word is reserved. A positive value indicates that the wheel was rotated forward, away from the user; a negative value indicates that the wheel was rotated backward, toward the user. One wheel click is defined as WHEEL_DELTA, which is 120.

    If the message is WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, or WM_NCXBUTTONDBLCLK, the high-order word specifies which X button was pressed or released, and the low-order word is reserved. This value can be one or more of the following values. Otherwise, mouseData is not used.

    Value Meaning
    XBUTTON1 0x0001 The first X button was pressed or released.
    XBUTTON2 0x0002 The second X button was pressed or released.

    So a simplified version of your callback could look like that:

    #include <iostream>
    #include <type_traits> // std::make_signed_t<>
    
    #include <windows.h>
    
    LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam)
    {
        if (nCode != HC_ACTION)  // Nothing to do :(
            return CallNextHookEx(NULL, nCode, wParam, lParam);
    
    
        MSLLHOOKSTRUCT *info = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
    
        char const *button_name[] = { "Left", "Right", "Middle", "X" };
        enum { BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_XBUTTON, BTN_NONE } button = BTN_NONE;
    
        char const *up_down[] = { "up", "down" };
        bool down = false;
    
    
        switch (wParam)
        {
    
        case WM_LBUTTONDOWN: down = true;
        case WM_LBUTTONUP: button = BTN_LEFT;
            break;
        case WM_RBUTTONDOWN: down = true;
        case WM_RBUTTONUP: button = BTN_RIGHT;
            break;
        case WM_MBUTTONDOWN: down = true;
        case WM_MBUTTONUP: button = BTN_MIDDLE;
            break;
        case WM_XBUTTONDOWN: down = true;
        case WM_XBUTTONUP: button = BTN_XBUTTON;
            break;
    
        case WM_MOUSEWHEEL:
            // the hi order word might be negative, but WORD is unsigned, so
            // we need some signed type of an appropriate size:
            down = static_cast<std::make_signed_t<WORD>>(HIWORD(info->mouseData)) < 0;
            std::cout << "Mouse wheel scrolled " << up_down[down] << '\n';
            break;
        }
    
        if (button != BTN_NONE) {
            std::cout << button_name[button];
            if (button == BTN_XBUTTON)
                std::cout << HIWORD(info->mouseData);
            std::cout << " mouse button " << up_down[down] << '\n';
        }
    
        return CallNextHookEx(NULL, nCode, wParam, lParam);
    }
    

    Regarding your main():

    Since your application has no windows, no messages will be sent to it and GetMessage() will never return. This renders the message pump youseless. A single call to GetMessage() is sufficient to give Windows the opportunity to call the installed hook callback. What is a problem though, is, that Code after the call to GetMessage() will never get executed because the only ways to end the program are closing the window or pressing Ctrl + C.

    To make sure UnhookWindowsHookEx() gets called, I'd suggest setting a ConsoleCtrlHandler:

    HHOOK hook = NULL;
    
    BOOL WINAPI ctrl_handler(DWORD dwCtrlType)
    {
        if (hook) {
            std::cout << "Unhooking " << hook << '\n';
            UnhookWindowsHookEx(hook);
            hook = NULL;  // ctrl_handler might be called multiple times
            std::cout << "Bye :(";
            std::cin.get();  // gives the user 5 seconds to read our last output
        }
    
        return TRUE;
    }
    
    int main()
    {
        SetConsoleCtrlHandler(ctrl_handler, TRUE);
        hook = SetWindowsHookExW(WH_MOUSE_LL, MouseHookProc, nullptr, 0);
    
        if (!hook) {
            std::cerr << "SetWindowsHookExW() failed. Bye :(\n\n";
            return EXIT_FAILURE;
        }
    
        std::cout << "Hook set: " << hook << '\n';
        GetMessageW(nullptr, nullptr, 0, 0);
    }