Linux X11 - 全局键盘钩子

37

是否有可能(或如何)在Linux X11(C++)中创建一个类似于Windows全局钩子(SetWindowsHookEx())的机制?

我想要能够捕获键盘事件,但是希望进一步传播。我正在尝试使用XGrabKey解决方案(例如xbindkeys),但是当我设置捕获键盘事件时,该事件将被“消费”。

该机制的要求如下:

  1. 全局/系统范围 - 捕获无论哪个窗口具有焦点的事件
  2. “捕获保持”和“捕获通过”的可能性
  3. 速度必须相当快

示例代码如下:

bool myFlagIsSet = false;
XEvent event;
while (true) {
    while (XPending(display) > 0) {
        usleep(SLEEP_TIME);
    }

    XNextEvent(display, &event);
    switch (e.type) {
        case KeyPress:
            if (myFlagIsSet) {
                //do not propagate
            }
            // propagate
            break;
        case KeyRelease:
            if (myFlagIsSet) {
                //do not propagate
            }
            // propagate
            break;
    }
}

在 Windows 上,我只需编写:

if(event.isConsumed()) {
    return LRESULT(1);
}
//...
return CallNextHookEx(hookHandle, nCode, wParam, lParam);
我也尝试过使用XUngrabKey和XSendEvent:
switch (event.type) {
    case KeyPress:
        if (myFlagIsSet) {
            //do not propagate
        }
        // propagate
        XUngrabKey(...);
        XSendEvent(..., &event);
        XGrabKey(...);
        break;
    case KeyRelease:
        ...
    }
很遗憾,XSendEvent出现了我不知道的问题 - 即使注释掉XGrabKey行也不发送此事件。
如果我注定要失败的话,请建议其他方法。
编辑:我想在Ubuntu Gnome上使用Compiz窗口管理器实现这一点。

1
此功能将取决于您使用的窗口管理器/桌面环境。 - Jonathan Hall
嘿@Xeon,你找到解决方案了吗?我正在尝试做完全相同的事情。(我更喜欢xcb,因为我可以在主线程上运行它。) - Noitidart
@Noitidart,不幸的是我不能。 - Xeon
哎呀 @Xeon,我已经成功地使用 gdk_add_window_filter 来做到这一点了,但它只在调用应用程序的窗口处于活动状态时才起作用,它并不是全局性的。根据某个用户的评论,在更改根目录上的窗口属性之后,xcb 可以全局工作,但我无法让它起作用。它可能适用于你 - https://dev59.com/BZTfa4cB1Zd3GeqPXeuS#tMbnnYgBc1ULPQZFVfjm - Noitidart
XPeekEvent能解决你的问题吗?https://www.x.org/releases/current/doc/man/man3/XNextEvent.3.xhtml - Animesh Sahu
5个回答

6

请尝试从此页面编译简单的代码:

http://webhamster.ru/site/page/index/articles/comp/367

这是获取全局键盘事件的示例。这个小应用程序作为xinput工作。

备注1: 将设备ID写入mian.cpp(通过运行xinput而不带参数获取ID):

sprintf(deviceId, "9");

注意2:编译命令:

gcc ./main.cpp -lstdc++ -lX11 -lXext -lXi

备注 3:编译之前,请安装libxi-dev软件包:

apt-get install libxi-dev

文件 main.h

#include <X11/Xlib.h>
#include <X11/extensions/XInput.h>

#ifdef HAVE_XI2
#include <X11/extensions/XInput2.h>
#endif

#include <X11/Xutil.h>
#include <stdio.h>
#include <stdlib.h>

extern int xi_opcode; /* xinput extension op code */

XDeviceInfo* find_device_info( Display *display, char *name, Bool only_extended);

#if HAVE_XI2
XIDeviceInfo* xi2_find_device_info(Display *display, char *name);
int xinput_version(Display* display);
#endif

文件 main.cpp

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include "main.h"
#include <ctype.h>
#include <string.h>

using namespace std;

int xi_opcode;

#define INVALID_EVENT_TYPE -1

static int motion_type = INVALID_EVENT_TYPE;
static int button_press_type = INVALID_EVENT_TYPE;
static int button_release_type = INVALID_EVENT_TYPE;
static int key_press_type = INVALID_EVENT_TYPE;
static int key_release_type = INVALID_EVENT_TYPE;
static int proximity_in_type = INVALID_EVENT_TYPE;
static int proximity_out_type = INVALID_EVENT_TYPE;

static int register_events(Display *dpy,
                           XDeviceInfo *info,
                           char *dev_name,
                           Bool handle_proximity)
{
    int            number = 0;    /* number of events registered */
    XEventClass        event_list[7];
    int            i;
    XDevice        *device;
    Window        root_win;
    unsigned long    screen;
    XInputClassInfo    *ip;

    screen = DefaultScreen(dpy);
    root_win = RootWindow(dpy, screen);

    device = XOpenDevice(dpy, info->id);

    if (!device) {
    printf("unable to open device '%s'\n", dev_name);
    return 0;
    }

    if (device->num_classes > 0) {
    for (ip = device->classes, i=0; i<info->num_classes; ip++, i++) {
        switch (ip->input_class) {
        case KeyClass:
        DeviceKeyPress(device, key_press_type, event_list[number]); number++;
        DeviceKeyRelease(device, key_release_type, event_list[number]); number++;
        break;

        case ButtonClass:
        DeviceButtonPress(device, button_press_type, event_list[number]); number++;
        DeviceButtonRelease(device, button_release_type, event_list[number]); number++;
        break;

        case ValuatorClass:
        DeviceMotionNotify(device, motion_type, event_list[number]); number++;
        if (handle_proximity) {
            ProximityIn(device, proximity_in_type, event_list[number]); number++;
            ProximityOut(device, proximity_out_type, event_list[number]); number++;
        }
        break;

        default:
        printf("unknown class\n");
        break;
        }
    }

    if (XSelectExtensionEvent(dpy, root_win, event_list, number)) {
        printf("error selecting extended events\n");
        return 0;
    }
    }
    return number;
}


static void print_events(Display *dpy)
{
    XEvent        Event;

    setvbuf(stdout, NULL, _IOLBF, 0);

    while(1) {
    XNextEvent(dpy, &Event);

    if (Event.type == motion_type) {
        int    loop;
        XDeviceMotionEvent *motion = (XDeviceMotionEvent *) &Event;

        printf("motion ");

        for(loop=0; loop<motion->axes_count; loop++) {
        printf("a[%d]=%d ", motion->first_axis + loop, motion->axis_data[loop]);
        }
        printf("\n");
    } else if ((Event.type == button_press_type) ||
           (Event.type == button_release_type)) {
        int    loop;
        XDeviceButtonEvent *button = (XDeviceButtonEvent *) &Event;

        printf("button %s %d ", (Event.type == button_release_type) ? "release" : "press  ",
           button->button);

        for(loop=0; loop<button->axes_count; loop++) {
        printf("a[%d]=%d ", button->first_axis + loop, button->axis_data[loop]);
        }
        printf("\n");
    } else if ((Event.type == key_press_type) ||
           (Event.type == key_release_type)) {
        int    loop;
        XDeviceKeyEvent *key = (XDeviceKeyEvent *) &Event;

        printf("key %s %d ", (Event.type == key_release_type) ? "release" : "press  ",
           key->keycode);

        for(loop=0; loop<key->axes_count; loop++) {
        printf("a[%d]=%d ", key->first_axis + loop, key->axis_data[loop]);
        }
        printf("\n");
    } else if ((Event.type == proximity_out_type) ||
           (Event.type == proximity_in_type)) {
        int    loop;
        XProximityNotifyEvent *prox = (XProximityNotifyEvent *) &Event;

        printf("proximity %s ", (Event.type == proximity_in_type) ? "in " : "out");

        for(loop=0; loop<prox->axes_count; loop++) {
        printf("a[%d]=%d ", prox->first_axis + loop, prox->axis_data[loop]);
        }
        printf("\n");
    }
    else {
        printf("what's that %d\n", Event.type);
    }
    }
}


// Определение версии библиотеки расширений, установленной для X11
int xinput_version(Display    *display)
{
    XExtensionVersion    *version;
    static int vers = -1;

    if (vers != -1)
        return vers;

    version = XGetExtensionVersion(display, INAME);

    if (version && (version != (XExtensionVersion*) NoSuchExtension)) {
    vers = version->major_version;
    XFree(version);
    }

#if HAVE_XI2
    /* Announce our supported version so the server treats us correctly. */
    if (vers >= XI_2_Major)
    {
        const char *forced_version;
        int maj = 2,
            min = 0;

#if HAVE_XI22
        min = 2;
#elif HAVE_XI21
        min = 1;
#endif

        forced_version = getenv("XINPUT_XI2_VERSION");
        if (forced_version) {
            if (sscanf(forced_version, "%d.%d", &maj, &min) != 2) {
                fprintf(stderr, "Invalid format of XINPUT_XI2_VERSION "
                                "environment variable. Need major.minor\n");
                exit(1);
            }
            printf("Overriding XI2 version to: %d.%d\n", maj, min);
        }

        XIQueryVersion(display, &maj, &min);
    }
#endif

    return vers;
}


// Поиск информации об устройстве
XDeviceInfo* find_device_info(Display *display,
                              char *name,
                              Bool only_extended)
{
    XDeviceInfo *devices;
    XDeviceInfo *found = NULL;
    int        loop;
    int        num_devices;
    int        len = strlen(name);
    Bool    is_id = True;
    XID        id = (XID)-1;

    for(loop=0; loop<len; loop++) {
    if (!isdigit(name[loop])) {
        is_id = False;
        break;
    }
    }

    if (is_id) {
    id = atoi(name);
    }

    devices = XListInputDevices(display, &num_devices);

    for(loop=0; loop<num_devices; loop++) {
    if ((!only_extended || (devices[loop].use >= IsXExtensionDevice)) &&
        ((!is_id && strcmp(devices[loop].name, name) == 0) ||
         (is_id && devices[loop].id == id))) {
        if (found) {
            fprintf(stderr,
                    "Warning: There are multiple devices named '%s'.\n"
                    "To ensure the correct one is selected, please use "
                    "the device ID instead.\n\n", name);
        return NULL;
        } else {
        found = &devices[loop];
        }
    }
    }
    return found;
}


int test(Display *display, char *deviceId)
{
    XDeviceInfo *info;

    Bool handle_proximity = True;

    info = find_device_info(display, deviceId, True);

    if(!info)
    {
      printf("unable to find device '%s'\n", deviceId);
      exit(1);
    }
    else
    {
      if(register_events(display, info, deviceId, handle_proximity))
         print_events(display);
      else
      {
        fprintf(stderr, "no event registered...\n");
        exit(1);
      }
    }

    return 0;
}


int main()
{
  Display *display;
  int event, error;

  // Инициируется указатель на текущий дисплей
  display = XOpenDisplay(NULL);
  if (display == NULL)
  {
    printf("Unable to connect to X server\n");
    exit(1);
  }

  // Проверяется наличие расширений
  if(!XQueryExtension(display, "XInputExtension", &xi_opcode, &event, &error))
  {
    printf("X Input extension not available.\n");
    exit(1);
  }

  // Проверяется версия расширения, она не должна быть нулевой
  if(!xinput_version(display))
  {
    printf("%s extension not available\n", INAME);
    exit(1);
  }

  char deviceId[10];
  sprintf(deviceId, "9");

  test(display, deviceId);

  XSync(display, False);
  XCloseDisplay(display);

  return 0;
}

4

我建议在输入设备层面上进行操作,而不是在X11级别上进行操作。 /dev/input/event<n> 可以提供输入事件。您可以在那里读取按键并决定它们是否应该进一步传播或被消耗。不幸的是,这方面没有真正的文档,但头文件linux/include/input.h相当自解释。此外,evdev维护者将很乐意回答电子邮件。


3
如果他的代码不是以root身份运行,这实际上根本不是一个选项;而且你需要重新实现X服务器处理的所有复杂键盘问题(布局、按键映射等);最后,你还需要处理热插拔和X服务器专有地占用设备的问题。 - Spudd86
1
@Spudd86:OP想要一个全局的钩子。对我来说,全局意味着它将接管所有进一步的处理。是的,这需要一些与热插拔的处理,但这也不太复杂。X服务器抓取设备也没有问题。已经做过类似的事情:一段时间以前,我编写了这样一个“全局钩子”,用于捕捉我的键盘多媒体按键,但是放行其他操作。 - datenwolf
1
取决于你要做什么,如果你想通过键盘上印刷的符号来引用键,那么你就必须处理 xkb 所做的所有疯狂事情,以便你可以正确地获取布局。(因为你希望它与 X 服务器正在执行的操作相同) - Spudd86

4
使用XTest扩展库的XTestFakeKeyEvent()函数来传播虚假按键按下/释放事件。

3
XSendEvent() 可能会发送它; 但由于它被广泛认为是一个安全漏洞,大多数程序会忽略带有 send_event 标志的UI事件。
标准的X11协议不允许这样做。 XInput 2.0扩展可能可以,但我怀疑;虽然Windows假定每个程序都监听一个单一的事件队列,以便程序可以拦截事件并防止它被发送到其他监听器的队列中,但每个X11客户端都有其自己独立的队列,所有注册对事件感兴趣的客户端都会在其队列中收到独立的副本。这意味着在正常情况下,一个错误的程序无法阻止其他程序运行;但这也意味着,在那些必须阻止其他客户端的客户端时,它必须执行服务器抓取,以防止服务器处理任何其他客户端的事件。

1

不知道这是否有帮助,但我刚在一些代码中发现了这个:



    void XFakeKeypress(Display *display, int keysym)
    { 
       XKeyEvent event;                     
       Window current_focus_window;         
       int current_focus_revert;

       XGetInputFocus(/* display = */ display, /* focus_return = */ 
          &current_focus_window, /* revert_to_return = */ &current_focus_revert);

       event.type = /* (const) */ KeyPress;
       event.display = display;
       event.window = current_focus_window;
       event.root = DefaultRootWindow(/* display = */ display);
       event.subwindow = /* (const) */ None;
       event.time = 1000 * time(/* tloc = */ NULL);
       event.x = 0;
       event.y = 0;
       event.x_root = 0;
       event.y_root = 0;
       event.state = /* (const) */ ShiftMask;   
       event.keycode = XKeysymToKeycode(/* display = */ display, 
          /* keysym = */ keysym);
       event.same_screen = /* (const) */ True;  

       XSendEvent(/* display = */ display, /* w = (const) */ InputFocus,
          /* propagate = (const) */ True, /* event_mask = (const) */ 
          KeyPressMask, /* event_send = */ (XEvent *)(&event));

       event.type = /* (const) */ KeyRelease;   
       event.time = 1000 * time(/* tloc = */ NULL);

       XSendEvent(/* display = */ display, /* w = (const) */ InputFocus,
          /* propagate = (const) */ True, /* event_mask = (const) */ 
          KeyReleaseMask, /* event_send = */ (XEvent *)(&event));
    }


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接