即使 Qt 应用程序最小化/在后台/失焦,如何捕获键盘和鼠标事件?

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

这里的需求只是检查是否有按键被按下或者是否有鼠标点击或移动发生。不需要捕获其他特定详细信息。
我也可以每隔 1 秒进行一次轮询,看看是否发生了任何鼠标或按键事件。

QAbstractNativeEventFilter会有帮助吗?
任何其他与平台无关的 C++ 库也很有用。


下面是捕获鼠标和键盘事件的示例代码 当应用程序处于焦点时:

#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();
}

您需要系统范围内的鼠标挂钩和键盘挂钩。它应该是在windows中注册的独立dll。然后你应该将它添加到 qt 。使用 installnativeeventfilter 并在可以使用纯 windows C 代码(来自 msvc)的过滤器中执行 SetWindowsHookEx() 两次。来自 dll 的 Ofc 挂钩内容应该在此过滤器模块中可见。总而言之,你能做到的机会并不多。我想你同意我的看法哈哈

忘记平台无关的东西

一些提示:
对于 hook 的回调函数,使用 CALLBACK type
dll应该放在windows\system左右。 windows 7 就够了
我敢打赌,您可以在原生过滤器的 .cpp 中执行 #pragma comment(lib, <hook_lib>) 。我的意思是 .lib 你会在 .dll
使用 .def 进行导出。 https://docs.microsoft.com/en-us/cpp/build/exporting-from-a-dll-using-def-files?view=msvc-160

为 3 个平台编写了最少的代码,但存在以下问题:

  • Linux:非常简单。它肯定会捕获 ctrlaltshift 等特殊键和鼠标点击。其他键盘输入有时是捕获的,有时不是。这是不一致的。如果使用XGrabKeyboard() API,那么所有的key都会被捕获。但是我不知道如何将这些密钥按原样传递给可见的应用程序。

  • Windows:工作正常而且简单明了!

  • Mac OSX: 还不行。可能缺少某些东西。如果我发现了什么,我会更新。


#include<QAbstractEventDispatcher>
#include<QAbstractNativeEventFilter>
#include<QGuiApplication>
#include<QDebug>

struct EventFilter : QAbstractNativeEventFilter
{
  EventFilter ();
  bool nativeEventFilter (const QByteArray& eventType, void* pMessage, long*) override;
};

int main (int argc, char *argv[])
{
  QGuiApplication application(argc, argv);
  EventFilter m_Filter;
  return application.exec();
}

#ifdef __linux__  // add in .pro file "linux: QT += x11extras" and "linux: LIBS += -lX11"
#include<QX11Info>

EventFilter::EventFilter () { qApp->eventDispatcher()->installNativeEventFilter(this); }

bool EventFilter::nativeEventFilter (const QByteArray& eventType, void* pMessage, long*)
{
  auto* const pEvent = static_cast<xcb_generic_event_t*>(pMessage);
  switch(pEvent->response_type)
  {
  case 28: case 37:        qDebug() << eventType << ": Keypress (special)" << pEvent->response_type; break;
  case 2: case 3: case 35: qDebug() << eventType << ": Keypress (general)"  << pEvent->response_type; break;
  case 85:                 qDebug() << eventType << ": Mouse Click"; break;
  default:                 qDebug() << eventType << ": <others> " << pEvent->response_type; break;
  }
  return false;
}
#endif

#ifdef WIN32
#include<Windows.h> // add in .pro file "win32: LIBS += -luser32"

HHOOK sHookKeyboard, sHookMouse;
EventFilter::EventFilter ()
{
    sHookKeyboard = ::SetWindowsHookExA(WH_KEYBOARD_LL,
                     [] (int i, WPARAM w, LPARAM l)
                     { qDebug() << i << w << l; return ::CallNextHookEx(sHookKeyboard, i, w, l); },
                                        0, 0);
    sHookMouse = ::SetWindowsHookExA(WH_MOUSE_LL,
                     [] (int i, WPARAM w, LPARAM l)
                     { qDebug() << i << w << l; return ::CallNextHookEx(sHookMouse, i, w, l); },
                                        0, 0);
}
bool nativeEventFilter (const QByteArray& eventType, void* pMessage, long*) {}
#endif

#ifdef __APPLE__ // add in .pro file "mac: LIBS += -framework Carbon"
#include<Carbon/Carbon.h>  // give required permission to the binary from "Application" settings

OSStatus HandleKeyboard (EventHandlerCallRef nextHandler, EventRef event, void* data)
{ qDebug() << "KeyPress event raised: "; return noErr; }
OSStatus HandleMouse (EventHandlerCallRef nextHandler, EventRef event, void* data)
{ qDebug() << "MouseClick event raised: "; return noErr; }

EventFilter::EventFilter ()
{
  EventTypeSpec eventKeyPress;
  eventKeyPress.eventClass = kEventClassKeyboard;
  eventKeyPress.eventKind = kEventRawKeyDown;
  InstallApplicationEventHandler(HandleKeyboard, 1, &eventKeyPress, nullptr, nullptr);

  EventTypeSpec eventMouseClick;
  eventMouseClick.eventClass = kEventClassMouse;
  eventMouseClick.eventKind = kEventMouseDown;
  InstallApplicationEventHandler(HandleMouse, 1, &eventMouseClick, nullptr, nullptr);
}
bool nativeEventFilter (const QByteArray& eventType, void* pMessage, long*) {}
#endif

已为以下 3 个平台编写了最少的工作代码。使用 Qt 是可选的。

#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: 
// ::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
// 
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