为什么XGrabKey会生成额外的focus-out和focus-in事件?

42

有没有人知道一个 xlib 函数可以在不失去原始焦点的情况下捕获按键事件?如何消除它?

(或者说“如何使用 XGrabKey() 而不生成抓取式焦点丢失”?)

(或者说“如何在系统级别消除 NotifyGrab 和 NotifyUngrab 焦点事件?”)

XGrabKey 会在按键被按下时失去焦点,并在松开按键时恢复焦点。

我想要捕获按键事件,而不将其泄漏到原始窗口中(就像 XGrabKey 可以做到的那样)。

参考资料:

  1. ...XGrabKey将窃取焦点... https://bugs.launchpad.net/gtkhotkey/+bug/390552/comments/8

  2. ...程序接收控制以响应按键组合。同时,程序已暂时获取焦点... 在XGrabKey(board)期间,发现哪个窗口已被聚焦

  3. ...XGrabKeyboard函数主动控制键盘并生成FocusIn和FocusOut事件... http://www.x.org/archive/X11R6.8.0/doc/XGrabKeyboard.3.html#toc3

  4. ...我看不到提供Metacity当前桌面更改行为(同时更改和显示弹出对话框)的方法而不会导致窗口的Grab类型失去焦点... https://mail.gnome.org/archives/wm-spec-list/2007-May/msg00000.html

  5. ...全屏模式不应在使用NotifyGrab的FocusOut事件中退出... https://bugzilla.mozilla.org/show_bug.cgi?id=578265

  6. 抓取键盘不允许更改焦点... grabbing keyboard doesnt allow changing focus

  7. 由Grabs生成的焦点事件(包括XGrabKeyboard的活动抓取和XGrabKey的被动抓取) http://www.x.org/releases/X11R7.6/doc/libX11/specs/libX11/libX11.html#Focus_Events_Generated_by_Grabs

  8. XGrabKey源代码:http://cgit.freedesktop.org/xorg/lib/libX11/tree/src/GrKey.c也许我们可以修改它以摆脱焦点失去事件?

  9. ActivateKeyboardGrab()中有"DoFocusEvents(keybd, oldWin, grab->window, NotifyGrab);": http://cgit.freedesktop.org/xorg/xserver/tree/dix/events.c

我正在编写一款将单个按键映射到多个按键组合(和鼠标移动)的软件:https://code.google.com/p/diyism-myboard/ 我已经在Windows中使用RegisterHotKey()和UnRegisterHotKey()实现了它:https://code.google.com/p/diyism-myboard/downloads/detail?name=MyBoard.pas 现在我想用XGrabKey()和XUngrabKey()将其迁移到Linux:https://code.google.com/p/diyism-myboard/downloads/detail?name=myboard.py 我已经创建了10美元的赏金来解决这个问题。我们需要更多支持者来提供赏金。https://www.bountysource.com/issues/1072081-right-button-menu-flashes-while-jkli-keys-move-the-mouse-pointer

1
你在写键盘记录器,对吧? ;) - Martin Thurau
3
不,我正在使用python-xlib编写一个键盘映射软件:https://code.google.com/p/diyism-myboard/ - diyism
3
我曾经(我查了一下是1997年)编写过一个键盘记录器,可以抓取键盘事件。我不知道它是否仍然能够编译/工作或者对你有任何用处,但如果你想要检查,我已经创建了一个gist:https://gist.github.com/robertklep/5124355 - robertklep
1
@robertklep xmodmap 只能将键组合映射到一个键,但无法将单个按键映射到键组合和鼠标移动。 - diyism
3
wxPython有RegisterHotKey可以在Microsoft Windows上直接连接操作系统功能,但在Linux上却无法实现。这是因为它不是钩子到"窗口管理器",而是直接与操作系统功能连接。你是否查看过X的源代码以获取捕获和重定向事件的钩子? - Anthon
显示剩余5条评论
8个回答

11

早在90年代初,我就曾研究过Irix、ultrix和solaris上的全局热键,因为在我的Acorn BBC计算机上很容易实现。最终我们决定通过一些专有代码,在xlib之下的一个非可移植层面上解决这个问题。由于我们的软件安装需要超级用户权限,因此我们能够插入适当的软件钩子作为守护程序。

对于Linux(现在),您应该在操作系统层面处理键盘事件来寻找软件解决方案。我会从这里开始查看:http://code.google.com/p/logkeys/

更通用的解决方案是使用一个USB输入和USB输出的小型PC板,它作为鼠标和键盘对计算机进行交互,并根据需要转换键盘按键。但如果要经常更改映射,则不太灵活。


11

我的当前代码(来自https://github.com/diyism/MyBoard/blob/master/myboard.py):

disp=Display()
screen=disp.screen()
root=screen.root

def grab_key(key, mod):
    key_code=string_to_keycode(key)
    #3rd: bool owner_events, 4th: pointer_mode, 5th: keyboard_mode, X.GrabModeSync, X.GrabModeAsync
    root.grab_key(key_code, mod, 0, X.GrabModeAsync, X.GrabModeAsync)
    root.grab_key(key_code, mod|X.LockMask, 0, X.GrabModeAsync, X.GrabModeAsync) #caps lock
    root.grab_key(key_code, mod|X.Mod2Mask, 0, X.GrabModeAsync, X.GrabModeAsync) #num lock
    root.grab_key(key_code, mod|X.LockMask|X.Mod2Mask, 0, X.GrabModeAsync, X.GrabModeAsync)

def main():
    grab_key('Shift_L', X.NONE)
    grab_key('Shift_R', X.NONE)
    while 1:
          evt=root.display.next_event()
          if evt.type in [X.KeyPress, X.KeyRelease]: #ignore X.MappingNotify(=34)
             handle_event(evt)

if __name__ == '__main__':
   main()

当我按下“shift”键时,焦点丢失了,释放它时焦点又回来了。

1
答案可能是在xlib中没有解决方案。 - diyism
也许我们可以修改XGrabKey的源代码,以消除焦点失去事件:http://cgit.freedesktop.org/xorg/lib/libX11/tree/src/GrKey.c - diyism
我的板子.py链接失效了,请使用这个链接:http://web.archive.org/web/20160107234343/https://diyism-myboard.googlecode.com/files/myboard.py - BladeMight

7

最后,正如您所知道的那样,Linux代表自由。我修改了Xserver以消除抓取式焦点丢失:

sudo apt-get build-dep xorg-server
apt-get source xorg-server
cd xorg-server-*
#modify or patch dix/events.c: comment off "DoFocusEvents(keybd, oldWin, grab->window, NotifyGrab);" in ActivateKeyboardGrab(), comment off "DoFocusEvents(keybd, grab->window, focusWin, NotifyUngrab);" in DeactivateKeyboardGrab()
sudo apt-get install devscripts
debuild -us -uc    #"-us -uc" to avoid the signature step
cd ..
sudo dpkg --install xserver-xorg-core_*.deb
#clear dependencies:
sudo apt-mark auto $(apt-cache showsrc xorg-server | grep Build-Depends | perl -p -e 's/(?:[\[(].+?[\])]|Build-Depends:|,|\|)//g')
sudo apt-get autoremove

而且我还需要在GTK上下文菜单中取消XGrabKeyboard:

sudo apt-get build-dep gtk+2.0
apt-get source gtk+2.0
cd gtk+2.0-*
#modify or patch it: add "return TRUE;" in first line of popup_grab_on_window() of gtk/gtkmenu.c
dpkg-source --commit
debuild -us -uc  #"-us -uc" to avoid the signature step, maybe need: sudo apt-get install devscripts
cd ..
sudo dpkg --install libgtk2.0-0_*.deb
#clear dependencies:
sudo apt-mark auto $(apt-cache showsrc gtk+2.0 | grep Build-Depends | perl -p -e 's/(?:[\[(].+?[\])]|Build-Depends:|,|\|)//g')
sudo apt-get autoremove

现在我的 myboard.py 运行良好。

我已将它从 GoogleCode 迁移到 GitHub:https://github.com/diyism/MyBoard


5

看起来 XQueryKeymap 可以帮到你。请参见下面我找到的C++源代码

/* compile with g++ keytest.cpp -LX11 -o keytest */
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

double gettime() {
 timeval tim;
 gettimeofday(&tim, NULL);
 double t1=tim.tv_sec+(tim.tv_usec/1000000.0);
 return t1;
}

int main() {
 Display *display_name;
 int depth,screen,connection;
 display_name = XOpenDisplay(NULL);
 screen = DefaultScreen(display_name);
 depth = DefaultDepth(display_name,screen);
 connection = ConnectionNumber(display_name);
 printf("Keylogger started\n\nInfo about X11 connection:\n");
 printf(" The display is::%s\n",XDisplayName((char*)display_name));
 printf(" Width::%d\tHeight::%d\n",
 DisplayWidth(display_name,screen),
 DisplayHeight(display_name,screen));
 printf(" Connection number is %d\n",connection);

 if(depth == 1)
  printf(" You live in prehistoric times\n");
 else
  printf(" You've got a coloured monitor with depth of %d\n",depth);

 printf("\n\nLogging started.\n\n");

 char keys_return[32];
 while(1) {
  XQueryKeymap(display_name,keys_return);
  for (int i=0; i<32; i++) {
   if (keys_return[i] != 0) {
    int pos = 0;
    int num = keys_return[i];
    printf("%.20f: ",gettime());
    while (pos < 8) {
     if ((num & 0x01) == 1) {
      printf("%d ",i*8+pos);
     }
     pos++; num /= 2;
    }
    printf("\n");
   }
  }
  usleep(30000);
 }
 XCloseDisplay(display_name);
}

请注意,以下内容并非我编写的经过测试的代码,只是我在互联网上找到的。


一旦它被捕捉,你就有责任将其困住,@diyism - hd1
1
XQueryKeymap可以捕获按键(不仅仅是捕获按键)吗?如何捕获它并且不泄露给系统? - diyism
2
它不会捕获事件,而是让您查看事件队列而不改变它。 - Anthon
就像我之前所说的那样,如果您选择这样做,您将负责陷阱它。 - hd1
这个答案不应该建议一些捕获键事件的方法吗?毕竟这才是最初的问题所在。 - ws_e_c421
显示剩余2条评论

3
我有一个想法,我相信它是可行的,但我需要上床睡觉了,无法亲自测试它,而且这并不美观,因为我认为在X中没有任何方法可以实现你想要的。下面是我考虑的步骤。简单来说:禁用X中的键盘,从较低级别的API读取事件,并选择性地将它们提供给X自己。你必须在X中禁用键盘,否则你可以查看事件,但不能停止它;你会与X一起阅读它,而不是拦截它。
具体步骤如下:
1) 运行 xinput -list以获取X正在使用的键盘
2) 运行xinput list-props id以查找设备启用属性
3) 运行xinput set-prop id prop 0 来禁用X中的设备。
xinput -list
xinput list-props 12 # 12 is the id of the keyboard in the list... (example # btw)
xinput set-prop 12 119 0 # 119 is the "Device Enabled" prop, we turn it off

我不知道xinput在xlib层面是如何工作的,为了实现简单,我只会在shell中调用它。

4) 打开/dev/input/eventX,这里的X是键盘设备。实际上,我会在/dev/input/by-id下搜索名称(在xinput -list中给出)并以这种方式打开它。由于这些权限通常非常限制,因此这可能需要root权限。

5) 从那里读取键盘输入:

输入事件的数据格式为:

struct input_event {
    int tv_sec; // time of the event
    int tv_usec; // ditto
    ushort type; // == 1 for key event
    ushort code; // key code, not the same as X keysyms, you should check it experimentally. 42 is left shift on mine, 54 is right shift
    int value; // for keys, 1 == pressed, 0 == released, 2 == repeat
}

ints是32位,ushorts是16位。由于您只对键盘输入感兴趣,因此可以相当简单地完成以下操作:
  • 读取并忽略8个字节。

  • 下一个字节应该是1,然后下一个字节是0。如果不是,跳过此事件

  • 下一个字节是键码的末尾,由于<255个键,所以这就足够了

  • 跳过下一个字节。

  • 读取下一个字节以查看是否按下或释放

  • 跳过下三个字节

6) 当您收到要捕获的事件时,请自行处理。否则,使用XSendEvent将其发送到X,以便可以正常处理。从/dev/input获取的硬件代码映射到适当的keysym可能有点棘手,但我相当确定xlib中有一个函数可以帮助解决这个问题。

7) 转到5并循环直到完成

8) 确保退出时将所有内容恢复原样,否则可能会破坏用户对X的键盘输入!

我建议使用第二个USB键盘进行测试,您可以使用/dev/input和xinput独立禁用和监听键盘,因此,如果崩溃,则仍然可以正常使用第一个键盘。(实际上,我认为使用第二个键盘故意这样做会相当酷,双倍的热键!)

但是,需要root并且可能将键盘与X“分离”不太美观,并且使用SendKey进行转发可能比说起来容易,但我相信这种方法会带给您最大的灵活性。


谢谢,这很合理但有些复杂。 - diyism

1

虽然我看到这个问题很旧,但它仍然相关,并且我发现一个比修改 xorg-server 更可移植的解决方法。

基本上,当抓取窗口是当前有焦点的窗口时,XGrabKey 不会发送焦点事件。

如果你观察活动窗口,并在焦点更改为新窗口时重置你的抓取,那么事件将不会生成。我发现观察 PropertyNotify 事件并查看 _NET_ACTIVE_WINDOW 窗口属性对获得那些信息很有效。

我还没有发现任何不良影响,或者说实话,设置抓取为当前活动窗口对任何设置均没有任何影响。


也许这种方法是正确的,但是myboard.py监听了太多的键盘事件,也许我的"commenting off DoFocusEvents"更有效。Myboard.py项目已经迁移到https://github.com/diyism/MyBoard,并且到目前为止,我已经每天使用这个项目8年了。 - diyism
只是为了我理解你的意思,你是在建议不要使用root.grab_key(),而是在每次窗口变化时使用active_window.grab_key()吗?这实际上听起来是一个合理的解决方案,似乎也是唯一可行的选择。 - phil294
我现在已经实施了这个,并且它运行得非常完美!这些都是我必须应用的所有更改。https://github.com/phil294/ahk_x11/commit/53fa4c7 - phil294
虽然不是所有系统都适用,但在Solus(或者一般的KDE环境)中,仍然更倾向于使用root grabbing(参见我的存储库上的问题)。如果有人能找到一个合适的解决方案,我会非常高兴。 - undefined

1

如果您想编写一个键盘映射软件,可以查看终端键盘输入库libtermkey,它能够识别类似于XTerm风格的鼠标位置/按钮报告、特殊按键(如箭头和功能键),包括"修改"键,如Ctrl-Left

例如,POE::Wheel::TermKey是"一个异步Perl封装器,封装了libtermkey库,提供了一种抽象的方式来读取终端程序中的按键事件。"


0
你可以使用XQueryKeymap来读取事件,然后可以使用XTestKey发送退格键事件,然后是你想要按下的键。更好的是,你可以注册快捷键来处理所有的键盘事件,然后使用XTestKey生成键盘事件。顺便说一句,KDE的"自定义快捷键"控制模块允许使用快捷键来生成按键操作。源代码

1
XQueryKeymap可以捕获事件,但无法陷阱事件。 - diyism
@diyism 但是我说过可以生成一个退格事件。你是那个给我点踩的人吗? - Ramchandra Apte
@Apte,退格键无法撤销已触发的鼠标移动。 - diyism
@diyism,所以您希望接收指针移动事件,但不希望指针被移动?我以为您只是重新映射键盘。 - Ramchandra Apte

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