Search code examples
c++qtmouseeventkeypresscapture

How to capture keyboard & mouse events even when the Qt app is minimized / in background / out of focus?


Here the requirement is just to check if any key was pressed or any mouse click or movement has happened. Capturing other specific details is not required.
I am fine to do every 1 second polling as well to see if any mouse or key event happened.

Will QAbstractNativeEventFilter be of any help?
Any other platform independent C++ library also will be useful.


Below is a sample code to capture mouse & keyboard events only when the app is in focus:

#include<QApplication>
#include<QDebug>
#include<QKeyEvent>
#include<QWidget>

struct Widget : public QWidget
{
  Widget ()
  {
    installEventFilter(this);
    grabKeyboard();
    grabMouse();
    setMouseTracking(true);
  }
  ~Widget () { qDebug() << "~Event()"; }

  bool eventFilter (QObject* const pObject,
                    QEvent* const pEvent) override
  {
    qDebug() << "Event: " << pEvent->type();
    if(pEvent->type() == QEvent::KeyPress)
    {
      QKeyEvent* const pKeyEvent = static_cast<QKeyEvent*>(pEvent);
      qDebug() << "Key event: " << pKeyEvent->key();
    }
    return false; //QObject::eventFilter(pObject, pEvent);
  }
};


int main (int argc, char *argv[])
{
  QApplication application(argc, argv);
  Widget widget;
  widget.show();
  return application.exec();
}

Solution

  • Have written minimal working code for the 3 platforms below. Using Qt is optional.

    #include<iostream>
    #include<thread>
    #include<chrono>
    
    struct Activity
    {
      Activity ();
    };
    
    // You may need to put Qt equivalent part here; The core logic is after main()
    #include<QGuiApplication>
    int
    main (int argc, char *argv[])
    {  // Qt specific; but it can be anything here
      QGuiApplication application(argc, argv);
      Activity activity;
      return application.exec();
    }
    
    #ifdef __linux__  // add in .pro file "linux: QT += x11extras" and "linux: LIBS += -lX11 -lXtst"
    #include<X11/Xlib.h>
    #include<X11/extensions/record.h>
    
    #define CHECK(EVENT) if(*pDatum == EVENT) std::cout << #EVENT
    void Handle (XPointer, XRecordInterceptData *pRecord)
    {
      using XRecordDatum = char;
      std::cout << pRecord->category << "---" << pRecord->data;
      if(auto* const pDatum = reinterpret_cast<XRecordDatum*>(pRecord->data))
      { CHECK(KeyPress); else CHECK(KeyRelease); else CHECK(ButtonPress); else CHECK(ButtonRelease); }
      ::XRecordFreeData(pRecord);
    }
    
    Activity::Activity ()
    {
      if(auto* const pDisplay = XOpenDisplay(nullptr))
      {
        XRecordClientSpec clients = XRecordAllClients;
        auto* pRange = ::XRecordAllocRange();
        pRange->device_events = XRecordRange8{KeyPress, ButtonRelease};
        auto context = ::XRecordCreateContext(pDisplay, 0, &clients, 1, &pRange, 1);
        ::XRecordEnableContextAsync(pDisplay, context, Handle, nullptr); // use with/without `...Async()`
    
        //  ::XRecordProcessReplies(pDisplay);
        ::XFlush(pDisplay);
        ::XSync(pDisplay, true);
      }
    // Use below functions by putting variables in global scope to stop capturing
    // Also refer: https://stackoverflow.com/questions/69711608/why-xrecorddisablecontext-is-not-working
    // ::XRecordDisableContext(pDisplay, context);
    //  ::XRecordFreeContext(pDisplay, context);
    //  ::XFree(pRange);
    }
    #endif
    
    #ifdef WIN32
    #include<Windows.h> // add in .pro file "win32: LIBS += -luser32"
    
    HHOOK sHookKeyboard, sHookMouse;
    Activity::Activity ()
    {
        sHookKeyboard = ::SetWindowsHookExA(WH_KEYBOARD_LL,
                         [] (int i, WPARAM w, LPARAM l)
                         { std::cout << i << w << l; return ::CallNextHookEx(sHookKeyboard, i, w, l); },
                                            0, 0);
        sHookMouse = ::SetWindowsHookExA(WH_MOUSE_LL,
                         [] (int i, WPARAM w, LPARAM l)
                         { std::cout << i << w << l; return ::CallNextHookEx(sHookMouse, i, w, l); },
                                            0, 0);
    }
    #endif
    
    #ifdef __APPLE__ // add in .pro file "mac: LIBS += -framework ApplicationServices"
    #include<ApplicationServices/ApplicationServices.h>
    // Add binary (& if you are it via running Qt, then that too) into the "privacy" settings of the ...
    // ... Mac system; System Preferences > Privacy settings; Without this the code will not work
    
    // https://developer.apple.com/forums/thread/109283
    // https://stackoverflow.com/questions/4556278/cgeventtapcreates-watching-keyboard-input-in-cocoa
    Activity::Activity ()
    {
      CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventScrollWheel) |
                         CGEventMaskBit(kCGEventLeftMouseDown) | CGEventMaskBit(kCGEventRightMouseDown);
      auto eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
                                       kCGEventTapOptionDefault, mask,
                           [] (CGEventTapProxy, CGEventType type, CGEventRef event, void*)
                           { std::cout << "captured: " << type; return event; }, this);
      if(eventTap == nullptr)
        return;
    
      CFRunLoopAddSource(CFRunLoopGetCurrent(),
                         CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0),
                         kCFRunLoopCommonModes);
      // Enable the event tap.
      CGEventTapEnable(eventTap, true);
    }
    #endif