在X11中监听键盘事件而不消耗它们 - 键盘钩子

14

我尝试编写一个程序,可以钩取Ubuntu(KDE)中键盘消息来发音每个按键的名称,而不干扰程序中键盘的正常操作(仅宣布按键名称)。

这是我的程序:

#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>

using namespace std;

void SendPressKeyEvent(Display *display, XKeyEvent xkey)
{
    Window current_focus_window;
    int current_focus_revert;
    XGetInputFocus(display, &current_focus_window, &current_focus_revert);
    xkey.type =  KeyPress;
    xkey.display = display;
    xkey.window = current_focus_window;
    xkey.root = DefaultRootWindow(display);
    xkey.subwindow = None;
    xkey.time = 1000 * time(0);
    xkey.x = 0;
    xkey.y = 0;
    xkey.x_root = 0;
    xkey.y_root = 0;
    xkey.same_screen = True;
    XSendEvent(display, InputFocus,  True, KeyPressMask, (XEvent *)(&xkey));
}

void SendReleaseKeyEvent(Display *display, XKeyEvent xkey)
{
    Window current_focus_window;
    int current_focus_revert;
    XGetInputFocus(display, &current_focus_window, &current_focus_revert);
    xkey.type =  KeyRelease;
    xkey.display = display;
    xkey.window = current_focus_window;
    xkey.root = DefaultRootWindow(display);
    xkey.subwindow = None;
    xkey.time = 1000 * time(0);
    xkey.x = 0;
    xkey.y = 0;
    xkey.x_root = 0;
    xkey.y_root = 0;
    xkey.same_screen = True;
    XSendEvent(display, InputFocus, True, KeyReleaseMask, (XEvent *)(&xkey));
}

void *TaskCode(void* arg)
{
    switch(*(int*)arg)
    {
    case 38:
        system("espeak -v en "  "\"a\"");
    }
    return 0;
}

int main()
{
    Display *display = XOpenDisplay(0);
    if(display == 0)
        exit(1);
    XGrabKeyboard(display, DefaultRootWindow(display), True, GrabModeAsync, GrabModeAsync, CurrentTime);
    XEvent event;
    while(true)
    {
        XNextEvent(display, &event);
        if(event.type == Expose)
        {

        }
        if(event.type == KeyPress)
        {
            SendPressKeyEvent(display,event.xkey);
            if(event.xkey.keycode == 38)
            {
                pthread_t thread;
                int thread_arg = event.xkey.keycode;
                pthread_create(&thread,0, TaskCode, (void*) &thread_arg);
            }
        }
        if(event.type == KeyRelease)
            SendReleaseKeyEvent(display,event.xkey);
    }
    XCloseDisplay(display);
}

这个程序只是针对键盘上的 a 键,但可以扩展到其他键。

然而,运行该程序时,某些程序(例如 Chromium)在其编辑框中不显示闪烁符号(光标)。同时,所有 KDE 热键都会被禁用。

如何解决这个问题?


1
pthread 大佬那里获取一些帮助。 - user2889419
(1) 学习线程;(2) 谁说 event.xkey.keycode 必须是 ASCII 码? - user207421
@EJP(1):我已经知道了,我正在寻找其他建议,如果有的话,(2):我没有说,它们显示什么? - Minimus Heximus
1
XGrabKeyboard不是实现此功能的可行基础。像每个好公民一样,跟踪焦点窗口并监听其键盘事件。 - n. m.
我脑海中没有... 我认为如果你使用XTestFakeKeyEvent而不是XSendEvent,你可能可以通过XGrabKeyboard来实现。 - n. m.
显示剩余2条评论
4个回答

9
这是一个简单的示例:
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <ctype.h>


int main ()
{
    Display* d = XOpenDisplay(NULL);
    Window root = DefaultRootWindow(d);
    Window curFocus;
    char buf[17];
    KeySym ks;
    XComposeStatus comp;
    int len;
    int revert;

    XGetInputFocus (d, &curFocus, &revert);
    XSelectInput(d, curFocus, KeyPressMask|KeyReleaseMask|FocusChangeMask);

    while (1)
    {
        XEvent ev;
        XNextEvent(d, &ev);
        switch (ev.type)
        {
            case FocusOut:
                printf ("Focus changed!\n");
                printf ("Old focus is %d\n", (int)curFocus);
                if (curFocus != root)
                    XSelectInput(d, curFocus, 0);
                XGetInputFocus (d, &curFocus, &revert);
                printf ("New focus is %d\n", (int)curFocus);
                if (curFocus == PointerRoot)
                    curFocus = root;
                XSelectInput(d, curFocus, KeyPressMask|KeyReleaseMask|FocusChangeMask);
                break;

            case KeyPress:
                printf ("Got key!\n");
                len = XLookupString(&ev.xkey, buf, 16, &ks, &comp);
                if (len > 0 && isprint(buf[0]))
                {
                    buf[len]=0;
                    printf("String is: %s\n", buf);
                }
                else
                {
                    printf ("Key is: %d\n", (int)ks);
                }
        }

    }
}

这并不是非常可靠,但大多数情况下它能正常工作(它正在显示我正在此框中输入的键)。您可能需要进一步调查为什么有时会失败 ;) 此外,原则上它无法显示热键。热键是被抓取的键,只有一个客户端可以获取一个抓取键。除了加载专门用于此目的的 X11 扩展(例如 XEvIE)之外,绝对不能在这里做任何事情。


1
看起来很完美。然而,我可以用XGrabKey解决问题,但是又出现了一个新的问题。我可能会在另一个问题中询问它。顺便说一下,一旦我使用你的代码完成我的代码,我会接受你的答案。谢谢。 - Minimus Heximus
1
你的代码在Ubuntu 13.10 KDE环境下对我来说还没有失败。至于快捷键,现在我不需要它们来使用espeak,但是我在答案中添加的第二个代码如果需要的话可以管理快捷键! - Minimus Heximus
1
所有都很好,但当应用程序进入后台时无法完成任务。 - Mohammad Kanan
不是在相应的活动窗口上监听FocusOut事件,而是全局监听_NET_ACTIVE_WINDOW的变化更可靠:https://dev59.com/zrnoa4cB1Zd3GeqPLCY9(但实际上,你应该使用recordlib,请参考我的其他答案) - phil294

7

感谢 n.m.答案parsa 的评论,这是我的最终代码:

#include <X11/Xlib.h>
#include <stdlib.h>
#include <iostream>

using namespace std;


void* TaskCode(void* parg)
{
    int keycode = *((int*)parg);
    cout<< "\n\n" << keycode << "\n\n";
    if(keycode == XKeysymToKeycode(XOpenDisplay(0),'a'))
        system("espeak -v en " "\"a\"");
    delete (int*)parg;
    return 0;
}

void Action(int keycode)
{
    pthread_t thread;
    pthread_attr_t  attrs;
    pthread_attr_init(&attrs);
    pthread_attr_setdetachstate(&attrs,PTHREAD_CREATE_DETACHED);
    pthread_attr_setstacksize(&attrs, 1000);
    int* pthread_arg = new int;
    *pthread_arg = keycode;
    pthread_create(&thread,&attrs, TaskCode, (void*) pthread_arg);
}

int MyX11ErrorHandler(Display *, XErrorEvent *error_event)
{
   cout << "\n\n" "An X11-Functions error occured. Probably the focused window was closed.""\n"
           "This error will be ignored." "\n";
   cout<< "error_code: " << (unsigned)error_event -> error_code << "\n";
   cout<< "minor_code: " << (unsigned)error_event -> minor_code << "\n";
   cout<< "request_code: " << (unsigned)error_event -> request_code << "\n";
   cout<< "resourceid: " << error_event -> resourceid << "\n";
   cout<< "serial; " << error_event -> serial << "\n";
   cout<< "type: " << error_event -> type << "\n\n";
   return 0;
}

int main()
{
    Display* display = XOpenDisplay(0);
    Window root = DefaultRootWindow(display);
    Window current_focus_window;
    int revert;

    XSetErrorHandler(MyX11ErrorHandler);

    XGetInputFocus(display, &current_focus_window, &revert);
    XSelectInput(display,current_focus_window,KeyPressMask | KeyReleaseMask | FocusChangeMask);

    while(true)
    {
        XEvent event;
        XNextEvent(display, &event);
        switch (event.type)
        {
            case FocusOut:
                if(current_focus_window != root)
                    XSelectInput(display, current_focus_window, 0);
                XGetInputFocus(display, &current_focus_window, &revert);
                if(current_focus_window == PointerRoot)
                    current_focus_window = root;
                XSelectInput(display, current_focus_window, KeyPressMask|KeyReleaseMask|FocusChangeMask);
                break;

            case KeyPress:
                Action(event.xkey.keycode);
                break;
        }
    }
}

在Qt Creator的项目.pro文件中添加以下内容:
LIBS += -lX11
LIBS += -lpthread
LIBS += -lXtst

任何改进建议都是受欢迎的。
为了实现归档,我还添加了抓取的最终代码:
#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>

using namespace std;

void* TaskCode(void* parg)
{
    int keycode = *((int*)parg);
    cout<< "\n\n" << keycode << "\n\n";
    system("espeak -v en " "\"a\"");
    delete (int*)parg;
    return 0;
}


void SendKeyEvent(Display *display, XEvent event)
{
    Window current_focus_window;
    XKeyEvent& xkey = event.xkey;

    int current_focus_revert;
    XGetInputFocus(display, &current_focus_window, &current_focus_revert);       
    xkey.state = Mod2Mask;

    XSendEvent(display, InputFocus,  True, xkey.type, (XEvent *)(&event));
}

int GrabKey(Display* display, Window grab_window, int keycode)
{
    unsigned int    modifiers       = Mod2Mask; // numlock on
    //Window          grab_window     = DefaultRootWindow(display);
    Bool            owner_events    = True;
    int             pointer_mode    = GrabModeAsync;
    int             keyboard_mode   = GrabModeAsync;

    XGrabKey(display, keycode, modifiers, grab_window, owner_events, pointer_mode, keyboard_mode);
    return keycode;
}

void UngrabKey(Display* display, Window grab_window, int keycode)
{
    unsigned int    modifiers       = Mod2Mask; // numlock on

   // Window grab_window = DefaultRootWindow(display);
    XUngrabKey(display,keycode,modifiers,grab_window);
}


void Action(int keycode)
{
    pthread_t thread;
    int* pthread_arg = new int;

    *pthread_arg = keycode;
    pthread_create(&thread,0, TaskCode, (void*) pthread_arg);
}

int main()
{
    Display*    display = XOpenDisplay(0);
    Window      root    = DefaultRootWindow(display);
    XEvent      event;

    int keycode = XKeysymToKeycode(display,'a');
    GrabKey(display,root,keycode);

    XSelectInput(display, root, KeyPressMask | KeyReleaseMask);
    while(true)
    {
        XNextEvent(display, &event);
        switch(event.type)
        {
            case KeyPress:
                Action(event.xkey.keycode);
            case KeyRelease:
                SendKeyEvent(display,event);
            default:
                break;
        }
    }

    XCloseDisplay(display);
}

除了与问题相关的代码不考虑语言布局之外,一切都很好。按下a键会打出a字符,无论语言布局如何!


2

1
谢谢Vladimir,你是指@InnocentBystander的解决方案吗?https://dev59.com/L14c5IYBdhLWcg3wvsfB#27693340 - Noitidart
需要 root 权限。 - phil294

1
听取所有事件的正确方法是使用 X Record Extension Library,它是 libXtst 的一部分,似乎安装在几乎每个 X 系统上。它的文档可以在 这里 找到,但由于文档不完整,您需要浏览先前的实现。这里 是一个很好的工作演示,这里 是一个更有能力和完整的实现。
下面是第一个示例的简化版本。
#include <stdio.h>
#include <X11/XKBlib.h>
#include <X11/extensions/record.h>

void key_pressed_cb(XPointer arg, XRecordInterceptData *d);

int scan(int verbose) {
    XRecordRange* rr;
    XRecordClientSpec rcs;
    XRecordContext rc;
    Display *dpy = XOpenDisplay(NULL);
    rr = XRecordAllocRange();
    rr->device_events.first = KeyPress;
    rr->device_events.last = ButtonReleaseMask;
    rcs = XRecordAllClients;
    rc = XRecordCreateContext (dpy, 0, &rcs, 1, &rr, 1);
    XFree (rr);
    XRecordEnableContext(dpy, rc, key_pressed_cb, NULL);
}


void key_pressed_cb(XPointer arg, XRecordInterceptData *d) {
    if (d->category != XRecordFromServer)
        return;
    
    int key = ((unsigned char*) d->data)[1];
    int type = ((unsigned char*) d->data)[0] & 0x7F;
    int repeat = d->data[2] & 1;

    if(!repeat) {
        switch (type) {
            case KeyPress:
                printf("key press %d\n", key);
                break;
            case KeyRelease:
                printf("key release %d\n", key);
                break;
            case ButtonPress:
                printf("button press %d\n", key);
                break;
            case ButtonRelease:
                printf("button release %d\n", key);
                break;
            default:
                break;
        }
    }
    XRecordFreeData (d);
}

int main() {
    scan(True);
    return 0;
}

将x1.c编译成可执行文件x1,需要链接X11和Xtst库。

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