3

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;
}
Swordfish
  • 12,971
  • 3
  • 21
  • 43
Charles
  • 35
  • 1
  • 5
  • *the Microsoft code segment is cleaner and should work.* - Where is that code? – Swordfish Nov 25 '18 at 10:41
  • @Swordfish I linked it on the "X button clicked" words (and [here](https://learn.microsoft.com/en-us/windows/desktop/learnwin32/mouse-clicks)). You can do a Ctrl+F for "XBUTTON1 and XBUTTON2" there, or I can paste their code segment unformatted here if you'd like. – Charles Nov 25 '18 at 10:55

1 Answers1

3

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);
}
Swordfish
  • 12,971
  • 3
  • 21
  • 43
  • Thank you so much for your answer. I am looking into all of the documentation you linked to better understand how all of this works. I'm still confused why the Microsoft docs would have code segments grabbing additional information out of the wParams. Maybe outdated information there? Anyways, thanks again! – Charles Nov 25 '18 at 11:31
  • @Charles No, there is no contradiction. `wParam` is just a name, it doesn't mean that it has the same meaning everywhere. I updated my answer with simplified code. – Swordfish Nov 25 '18 at 11:38
  • I suppose that makes sense. So in the Microsoft docs that I linked, the GET_XBUTTON_WPARAM and GET_WHEEL_DELTA_WPARAM methods don't actually fetch them from the same type of wParams that we're using here? Also, your edited code you supplied doesn't include the differentiating of the X buttons like your original answer had. – Charles Nov 25 '18 at 11:47
  • @Charles *your edited code you supplied doesn't include [...]* – Sorry, fixed that. – Swordfish Nov 25 '18 at 12:09
  • @Charles Documentation about the various Windows Messages usualy only applies when they are received through a window procedure. Typical sentence in the MSDN: *A window receives this message through its WindowProc function.* That hooks also use `WM_foobar` is rather a coincidence. – Swordfish Nov 25 '18 at 12:13
  • @Charles Speaking of *Window* Messages ... The message pump in your `main()` is pointless. Console applications without window don't receive window messages so the whole loop could be replaced with `GetMessage(nullptr, nullptr, 0, 0);`, it will never return. – Swordfish Nov 25 '18 at 12:21
  • Ah, I see. That misunderstanding would explain my previous confusion more. I'm working a bit beyond my level of experience here, but I'm happy and eager to learn regardless! One last question for you for now is about the including of the cstdint library. I see your comment about why you included it, but int16_t _appears_ to work without including it OR using the namespace std:: prefix. Is including the library and the prefix more of a redundancy? – Charles Nov 25 '18 at 12:25
  • The main method message pump I found from another stack overflow question, but the console application is just temporary. Eventually I will move it to its own window when I get all of the internal functionality figured out. – Charles Nov 25 '18 at 12:28
  • @Charles It is possible that `` (or one of the other files it includes), includes ``, the C version of `` so it can be used without specifying its namespace. However, thats undocumented. The C++ standard says: you want to use `std::int16_t` then include ``. `short` should/could/maybe is also 16 bits wide, but I prefer to play it save. And using `std::int16_t` makes it obvious why there is a cast and why. – Swordfish Nov 25 '18 at 12:30
  • I see. I'm not too familiar with the C++ standards yet, but I was curious. I really appreciate all of your help. I'd love to have you as a point of contact for the occasionally question, but here on stack overflow may be the best place to ask them anyhow so that others can view them as well. I would take this conversation to a chat like it's requesting me to, but my reputation is not high enough yet. Thanks again! – Charles Nov 25 '18 at 12:40
  • As far as playing it safe goes, using `std::int16_t` is as good as it gets, but not necessarily good enough. Many of the [fixed width integer types](https://en.cppreference.com/w/cpp/types/integer) are an optional feature, and any conforming implementation can opt to not provide them. – IInspectable Nov 25 '18 at 13:29
  • @IInspectable So what would you suggest? Test the `WORD` in an other way for negative values? // edit: I just saw `std::make_signed` ... I'll update my answer. – Swordfish Nov 25 '18 at 13:34
  • @Swordfish "*Since your application has no windows, no messages will be sent to it and GetMessage() will never return. This renders the message pump youseless*" - not true. Low-level hooks run in the thread that installs them, and that requires that thread to have a message **loop**, as stated in the documentation: "*This hook is called in the context of the thread that installed it. **The call is made by sending a message to the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop**.*" A single call to `GetMessage()` is NOT sufficient. – Remy Lebeau Nov 25 '18 at 18:56
  • @Swordfish However, you can use `SetConsoleCtrlHandler()` to setup a handler that sends its own message to the `main` thread to break the message loop. – Remy Lebeau Nov 25 '18 at 18:58
  • @RemyLebeau *not true. Low-level hooks run in the thread that installs them, and that requires that thread to have a message loop, as stated in the documentation* – Usually I'm all in for standards and specifications, but in this case it's \*really\* useless since `GetMessage()` will dispatch all messages to the hook_proc. It won't return with a message for \*us\* to handle. – Swordfish Nov 25 '18 at 19:13
  • @RemyLebeau *handler that sends its own message to the main thread to break the message loop* – I have a ctrl handler. But to receive a message in the main thread I'd have to create a window. – Swordfish Nov 25 '18 at 19:26
  • 1
    @Swordfish "*to receive a message in the main thread I'd have to create a window*" - actually no, you can use a **thread message** instead of a **window message**. `GetMessage()` supports both when its HWND parameter is NULL. Your handler can use `PostThreadMessage()` to post a message to the main thread, and then `GetMessage()` can return that message to `main` – Remy Lebeau Nov 25 '18 at 20:55
  • @RemyLebeau Oh, thats nice :) Didn't know of `PostThreadMessage()`. – Swordfish Nov 25 '18 at 21:05
  • @Swordfish Thank you for continuing to expand upon your answer. About the main method issue. As I mentioned, I am planning to move my console application to it's own window in the future. I realize my supplied code was never going to reach the unhook, but I had it there for when I get to adding more functionality. Was I approaching the message pump incorrectly when it comes to a window application? I appreciate you taking the time to add a ctrl handler, but that won't be necessary when it's no long a console application, correct? – Charles Nov 26 '18 at 07:41
  • @Charles *Was I approaching the message pump incorrectly when it comes to a window application?* – Your loop `while(GetMessage( /* ... */ ) > 0)` is incorrect because [`GetMessage()`](https://learn.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getmessage) says "*If the function [GetMessage()] retrieves a message other than WM_QUIT, the return value is nonzero.*", which includes negative values. See the documentation of `GetMessage()` for sample code. – Swordfish Nov 26 '18 at 10:57
  • @Charles *a ctrl handler [...] won't be necessary when it's no long a console application, correct?* – Yes, you don't need to handle that when your application doesn't deal with a console. Please keep in mind that non console applications can create console windows too. – Swordfish Nov 26 '18 at 11:01