c++windowshotkeyssetwindowshookex

How to assign multiple LowLevel HotKeys with SetWindowsHookExA and LowLevelKeyboardProc


I'm trying to write a class for LowLevel Global HotKeys. The Idea is that one instance of that Class represents a HotKey or HotKey Sequence, like Alt+SHift+G and that works. When I create now a second instance of that class for a second HotKey, that overwrites my first, only the second or last one can be triggered. Any idea how I could extend this to work with more instances? Can I make the LowLevelKeyboardProc a non static member method of my class? Maybe this would fix my issue.

WinKeyHandler.h:

class WinKeyHandler : public AbstractKeyHandler
{
public:
    WinKeyHandler();
    ~WinKeyHandler() override;

    bool registerKey(const QKeySequence &keySequence) override;
    bool isHotkeyTriggered(void* message) override;

private:
    KeySequenceToWinKeyCodeTranslator mKeyCodeMapper;
    static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
    HHOOK mLowLevelKeyboardHook;

    QVector<DWORD> mPressedKeys;
    unsigned int mPressedModifiers;
    unsigned int mPressedKey;
    bool mTriggered;
    KeyCodeCombo mKeySequence;

    DWORD translateVkCode(DWORD vkcode);
    void handleKeySequence();
    void handleKeyPress(DWORD vkCode);
    void handleKeyRelease(DWORD vkCode);
    void resetKeys();
};

WinKeyHandler.cpp:

WinKeyHandler * mWinKeyHandlerReference;

WinKeyHandler::WinKeyHandler()
{
    resetKeys();
    mWinKeyHandlerReference = this;
}

WinKeyHandler::~WinKeyHandler()
{
    UnhookWindowsHookEx(mLowLevelKeyboardHook);
}

bool WinKeyHandler::registerKey(const QKeySequence &keySequence)
{
    mKeySequence = mKeyCodeMapper.map(keySequence);
    mLowLevelKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, nullptr, 0);

    return mLowLevelKeyboardHook != nullptr;
}

bool WinKeyHandler::isHotkeyTriggered(void* message)
{
    Q_UNUSED(message)

    return false;
}

LRESULT CALLBACK WinKeyHandler::LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode == HC_ACTION)
    {
        auto vkCode = mWinKeyHandlerReference->translateVkCode(PKBDLLHOOKSTRUCT(lParam)->vkCode);
        switch (wParam)
        {
            case WM_KEYDOWN:
            case WM_SYSKEYDOWN:
                    mWinKeyHandlerReference->handleKeyPress(vkCode);
                break;
            case WM_KEYUP:
                mWinKeyHandlerReference->handleKeyRelease(vkCode);
                break;
        }

        mWinKeyHandlerReference->handleKeySequence();
    }
    return CallNextHookEx(nullptr, nCode, wParam, lParam);
}

DWORD WinKeyHandler::translateVkCode(DWORD vkCode)
{
    if(vkCode == VK_LCONTROL || vkCode == VK_RCONTROL) {
        return VK_CONTROL;
    } else if(vkCode == VK_LSHIFT || vkCode == VK_RSHIFT) {
        return VK_SHIFT;
    } else if(vkCode == VK_LMENU || vkCode == VK_RMENU) {
        return VK_MENU;
    } else {
        return vkCode;
    }
}

void WinKeyHandler::handleKeySequence()
{
    if(mKeySequence.modifier == mPressedModifiers && mKeySequence.key == mPressedKey) {
        if(!mTriggered) {
            emit triggered();
            resetKeys();
        }
        mTriggered = true;
    } else {
        mTriggered = false;
    }
}

void WinKeyHandler::handleKeyPress(DWORD vkCode)
{
    if(!mPressedKeys.contains(vkCode)) {
        mPressedKeys.append(vkCode);

        if(vkCode == VK_CONTROL || vkCode == VK_MENU || vkCode == VK_SHIFT) {
            mPressedModifiers |= vkCode;
        } else {
            mPressedKey = vkCode;
        }
    }
}

void WinKeyHandler::handleKeyRelease(DWORD vkCode)
{
    if(mPressedKeys.contains(vkCode)) {
        mPressedKeys.removeOne(vkCode);

        if(vkCode == VK_CONTROL || vkCode == VK_MENU || vkCode == VK_SHIFT) {
            mPressedModifiers ^= vkCode;
        } else {
            mPressedKey = 0;
        }
    }
}

void WinKeyHandler::resetKeys()
{
    mPressedKey = 0;
    mPressedModifiers = 0;
}

Solution

  • Multiple hooks can be registered at the same time, they will get chained together (which is why each hook must call CallNextHookEx(), so the next hook in the chain gets called).

    Your problem is not because you have multiple hooks registered, but rather is because you are using a global variable mWinKeyHandlerReference, which can only refer to one class instance at a time. It is being set to refer to the last instance created, which means all hooks send their events to that one instance.

    No, you cannot directly use a non-static class method as the hook procedure, because of the implicit this parameter that would be needed, which the hook won't know how to pass in.

    What you can do instead is have the class create a thunk - a block of executable memory allocated dynamically via VirtualAlloc() with PAGE_EXECUTE rights containing just enough CPU instructions to forward its input parameters to a non-static method using the object's this pointer - and then you can use that thunk as the hook procedure. That will allow you to create per-instance hook procedures that use individual this pointers.

    See Callback of member functions through 3d party library and Thunking in Win32: Simplifying Callbacks to Non-static Member Functions.