Linux中使用XLib注册系统级热键
本文介绍在Linux X11环境下如何注册操作系统级的热键。使你的应用程序C/C++/Java…能够在没有焦点的情况下触发其功能。本文所实现的功能源自一个Linux触摸屏软键盘的项目,该项目需要在一台工控机上部署Linux(Lubuntu),并用面板上的GPIO按键(转USB键盘芯片发送标准键值给PC)随时呼出软键盘进行操作。
思路灵感来自于这篇blog:http://blog.163.com/ojb_123/blog/static/2417742420094138384658/ ,在此感谢作者。
知识点
- X Window是Linux图形系统,X client(一般是各种应用程序)跟X server(Linux提供)进行通信从而在屏幕上显示各式窗体和图像。
- 「Linux注册系统热键,在应用程序没有焦点的情况触发功能事件」,这样的需求只会存在于使用X Window的情况,当使用命令行终端时并不太可能有这样的需求。
- XLib中提供
XGrabKeyboard()
和XGrabKey()
函数供抓取键盘事件,前者用来让应用程序打开键盘监听,以后在获取焦点的情况下就能捕获任意键盘事件;后者是我们需要用到的,指定特定的键值捕获,并且以passtive
的方式捕获,也就是无论是否能获取焦点,KeyEvent事件都能透传到我们的应用程序,详细描述请查阅XLib手册:https://tronche.com/gui/x/xlib/input/keyboard-grabbing.html - Linux下的XLib动态库是以
libX11.so
的命名方式提供的,g++
编译时需加上-lX11
参数表示引用libX11.so
。/usr/lib/
路径下有我们需要的lib,并且libX11.so
是一个link,指向另外的名字中带上版本号的.so,比如libX11.so.0.6.0
。 - X Window虽然在操作系统运行时就提供了,但XLib和所要用的头文件,需要另外下载:
sudo apt install libx11-dev
- Linux X环境下,如果按住一个按键不松开,其Press和Release事件会一直循环触发,如果程序中需要对物理键盘一次敲击只响应一次,需要自己加一点逻辑判断。
- 基于C/C++的全局注册模块编译出来后,可以通过JNI技术可以轻松为上层JAVA程序使用,甚至你可以使用JavaFX构建美观的GUI,实际上我就是这么干的,留到下次写文章来介绍。
1. 准备工作
- 一台Lubuntu16.04,或者其他Ubunut LTS版本,或者其他Debian。(个人偏好使用Lubuntu长期支持版)
sudo apt install libx11-dev
sudo apt install g++
sudo apt install vim
sudo apt install make
2. 编码
vim hotkey.cpp
#include <stdio.h>
#include <X11/Xlib.h>
int main(){
Window root;
XEvent e;
int F4, F6;
//Open the display
Display *dpy = XOpenDisplay(0);
if(!dpy)
return 1;
root = DefaultRootWindow(dpy);
F4 = XKeysymToKeycode(dpy, XStringToKeysym("F4"));
F6 = XKeysymToKeycode(dpy, XStringToKeysym("F6"));
//Register the keys
XGrabKey(dpy, F4, 0, root, True, GrabModeAsync, GrabModeAsync);
XGrabKey(dpy, F6, 0, root, True, GrabModeAsync, GrabModeAsync);
//Wait for events
for(;;){
XNextEvent(dpy, &e);
if(e.type == KeyPress){
if(e.xkey.keycode == F4)
break;
else if(e.xkey.keycode == F6)
printf("F6 pressed\n");
}
}
XUngrabKey(dpy, F4, 0, root);
XUngrabKey(dpy, F6, 0, root);
}
OK,最原始的测试DEMO就写好了,这份代码主要是打通和验证注册热键的功能。下面进行编译测试:
g++ -o hotkey.o hotkey.cpp -lX11
不出意外的话就可以编译出来,在X Window环境下Ctrl+Alt+T
打开图形界面下的终端,而不要直接Ctrl+Alt+F1
打开纯命令行,执行编译出来的hotkey.o
,程序应该在for(;;)
死循环中停住,然后按下F6功能键,则输出F6 pressed
,按下F4则退出程序,截图如下:
3. 剖析和魔改
观察上述代码:
- 首先定义了两个int F4,F6,对其用
F4 = XKeysymToKeycode(dpy, XStringToKeysym("F4"))
进行赋值,可以看出XStringToKeysym()
一定是一个能把字符串解析成键值的高端玩意儿。 - 接着用
XGrabKey(dpy, F4, 0, root, True, GrabModeAsync, GrabModeAsync);
,对F4和F6按键进行系统级的热键注册。 - 最后写了一个死循环监听键盘事件,在监听循环中,判断是否是KeyPress事件,如果是的话,键值为F4就退出,键值为F6就输出字符串。
- 程序的最后解除了针对F4和F6的键盘监听
代码非常容易理解,于是我们尝试深入一下头文件,并修改代码。
刚才提到XStringToKeysym()
这个函数挺有趣的,能将字符串转成键值,但我并不觉得好用,因为键盘上那么多按键,我也不知道除了F功能键以外的按键应该用什么字符串来表达,我还是希望能够用确切的键值来表示,最好是整型。
于是执行
grep -rn "XStringToKeysym" /usr/include/X11/
#搜索到XStringToKeysym返回的是KeySym类型
grep -rn "KeySym" /usr/include/X11/
#搜索到KeySym是CARD32的别名
grep -rn "CARD32" /usr/include/X11/
#搜索到CARD32是unsigned int的别名
OK,确实返回的是一个整型,那么我们直接用确切的整型吧,就别用XStringToKeysym
再去转了,也方便以后用其他键值的时候不知道输入啥字符串。
那么整型的键值在哪里定义的呢?同样的,使用暴力的搜索命令
grep -rn "F4" /usr/include/X11/
找到两个比较靠谱的位置,打开/usr/include/X11/keysymdef.h
瞧瞧吧:
全是我们想要的!OK,丢掉XStringToKeysym
,潇洒地用键值吧。
4. 重复触发问题
我们在Windows下处理键盘事件,几乎都有KeyPress
和KeyRelease
可以处理,XLib同样也有,在事件枚举中同样提供了KeyRelease
事件条目,理论上如果按键按下不松开,那么会触发一次KeyPress
事件,直到按键松开,再触发一次KeyRelease
事件。然而,XLib为了解决光标键,回车键,DEL键等按键的连续响应问题,将所有按键都设置成AutoRepeat了。这就有点讨厌了,设想一下我们设计一个软键盘程序,在用户按一下F6的时候呼出,再按一下F6的时候隐藏,如果操作人员按F6的时间有点长,那这个软键盘窗体会一直在屏幕上闪动,不停地切换显示/隐藏的状态。因此我们在程序中如果想只响应一次,就必须做出调整。
借鉴这个ISSUE:https://stackoverflow.com/questions/2100654/ignore-auto-repeat-in-x11-applications
可以看到,这个ISSUE中,有人用e.xkey.time
,也就是触发时间来处理这个问题,但给出的代码还是有些晦涩,那么我们来自己动手研究一下。
可以看出每个键盘事件响应时,都会带上xkey.time
这个值,目测是事件响应的时间,那么我们继续暴力搜索,寻找蛛丝马迹吧:
Xlib.h和X.h中有如下定义:
OK,Time是一个unsigned long int
,长整。那么我们触发时将其输出一下试试:
修改代码:
for(;;){
XNextEvent(dpy, &e);
if(e.type == KeyPress){
if(e.xkey.keycode == F4)
break;
else if(e.xkey.keycode == F6)
printf("F6 pressed, press time : %ld\n", e.xkey.time);
} else if(e.type == KeyRelease){
if(e.xkey.keycode == F6)
printf("F6 pressed, release time : %ld\n", e.xkey.time);
}
}
OK,我们看出端倪来了,原来KeyPress会在上一次KeyRelease发生后立即触发,二者的时间是相同的,那么好了,只要我们记录一下上一次KeyRelease的时间,下一次KeyPress发生时再判断一下,如果KeyPress的时间跟上一次KeyRelease的时间不同,则表示是两次独立的事件,如果相同,则表示用户的手没有松开按键,不应该响应。
编码如下:
//if press-release event repeat,the last release time = the next press time
unsigned long lastReleaseTime = 0;
//Wait for events
for(;;){
XNextEvent(dpy, &e);
if(e.type == KeyPress && e.xkey.time != lastReleaseTime){
if(e.xkey.keycode == F4)
//do something...
else if(e.xkey.keycode == F6)
//do something...
}else if(e.type == KeyRelease){
lastReleaseTime = e.xkey.time;
}
}
OK,本文到此结束了,可以成功注册Linux系统热键了,后面还会写一篇小文介绍一下如何将这个模块编译成.so动态链接库,使用JNI技术,供上层JAVA调用,并使用JavaFX设计一个美观的软键盘。
最后上一张软键盘做好的截图: