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;
}
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
, orWM_RBUTTONUP
.
lParam
[in]
Type:LPARAM
A pointer to anMSLLHOOKSTRUCT
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
:
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 asWHEEL_DELTA
, which is 120.If the message is
WM_XBUTTONDOWN
,WM_XBUTTONUP
,WM_XBUTTONDBLCLK
,WM_NCXBUTTONDOWN
,WM_NCXBUTTONUP
, orWM_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);
}