前言
在Linux桌面开发中,监控全局键盘鼠标输入是实现快捷键管理器、自动化脚本、屏幕录制工具等功能的核心技术。
X Window System(X11)提供了丰富的API来捕获和处理输入事件。
本文将深入探讨如何使用X11监听全局键盘鼠标输入,构建功能强大的输入监控工具。
X11输入事件系统架构
1.1 输入事件传递机制
text
输入设备 → X Server → 焦点窗口 → 应用程序
X11采用事件驱动模型,所有输入事件首先由X Server接收,然后分发给具有焦点的客户端窗口。
1.2 输入事件类型
键盘事件:KeyPress、KeyRelease
鼠标事件:ButtonPress、ButtonRelease、MotionNotify
焦点事件:FocusIn、FocusOut
输入法事件:MappingNotify
1.3 事件掩码与选择
X11使用事件掩码机制,客户端需要显式选择要接收的事件类型:
c++
// 常用输入事件掩码
KeyPressMask | KeyReleaseMask // 键盘按下/释放
ButtonPressMask | ButtonReleaseMask // 鼠标按钮
PointerMotionMask // 鼠标移动
EnterWindowMask | LeaveWindowMask // 窗口进入/离开
基础键鼠事件监听
由于X11原生的XSelectInput无法实现全局监听,而XGrab/XUngrab族函数在执行期间会独占键鼠活动,影响原有的功能。因此,只能通过额外的扩展来实现监听。
2.1 键码解析
由于X11使用KeyCode描述具体的物理按键,因此需要通过XkbKeycodeToKeysym将KeyCode转换为KeySym符号,进而获取按键的具体描述。
cpp
KeySym LookupKeysym(Display *display, KeyCode key, KeyButMask mask) {
// 获取按键的映射
XkbDescPtr xkb = XkbGetMap(display.get(), XkbAllComponentsMask, XkbUseCoreKbd);
unsigned mods_rtrn;
KeySym sym;
// 转换字符码
XkbTranslateKeyCode(xkb, key, mask, &mods_rtrn, &sym);
XkbFreeClientMap(xkb, 0, True);
return sym;
}
高级键鼠事件监听
3.1 XInput扩展
cpp
int main()
{
Display *display = XOpenDisplay(NULL);
if (!display) {
fprintf(stderr, "无法打开 X 显示\n");
return -1;
}
int eventv, xi_opcode;
if (!XQueryExtension(display, "XInputExtension", &xi_opcode, &eventv,
NULL)) {
fprintf(stderr, "XInput 扩展不可用\n");
return -1;
}
XIEventMask event_mask;
unsigned char mask[2] = {0}; // 位掩码
// XI_ButtonPress 和 XI_ButtonRelease
XISetMask(mask, XI_ButtonPress);
XISetMask(mask, XI_KeyPress);
event_mask.deviceid = XIAllDevices; // 这里必须指定XIAllDevices,才能监听全局的点击事件
event_mask.mask_len = sizeof(mask);
event_mask.mask = mask;
XISelectEvents(display, DefaultRootWindow(display), &event_mask,1);
XFlush(display);
XEvent event;
XGenericEventCookie* cookie;
XIDeviceEvent* dev_event;
while (1) {
XNextEvent(display, &event);
// 检查是否是XInput2事件
if (event.type != GenericEvent) continue;
cookie = &event.xcookie;
// 检查是否是XInput扩展事件
if (cookie->extension != xi_opcode) continue;
// 获取事件数据
if (!XGetEventData(display, cookie)) continue;
// 处理不同的事件类型
switch (cookie->evtype) {
case XI_ButtonPress: {
dev_event = (XIDeviceEvent*)cookie->data;
printf("=== Mouse Button Press ===\n");
printf(" Button: %d\n", dev_event->detail);
printf(" Root Coordinates: (%f, %f)\n", dev_event->root_x,
dev_event->root_y);
break;
}
case XI_KeyPress: {
dev_event = (XIDeviceEvent*)cookie->data;
printf("=== Key Press ===\n");
printf(" Key: %d\n", dev_event->detail);
printf(" Mask: %d\n", dev_event->mods.effective);
break;
}
}
// 释放事件数据
XFreeEventData(display, cookie);
}
XCloseDisplay(display);
return 0;
}
3.2 Record扩展
cpp
extern "C" void event_callback(XPointer closure, XRecordInterceptData* data) {
if (data->category != XRecordFromServer) {
XRecordFreeData(data);
return;
}
xEvent* xevent = reinterpret_cast<xEvent*>(data->data);
if (xevent->u.u.type == ButtonPress) { // ButtonPress
short x = xevent->u.keyButtonPointer.rootX;
short y = xevent->u.keyButtonPointer.rootY;
printf("=== Mouse Button Press ===\n");
printf(" Button: %d\n", xevent->u.u.detail);
printf(" Root Coordinates: (%f, %f)\n", x, y);
} else if (xevent->u.u.type == KeyPress) {
printf("=== Key Press ===\n");
printf(" Key: %d\n", xevent->u.u.detail);
printf(" Mask: %d\n", xevent->u.keyButtonPointer.state);
printf("Key Press = %d %d\n", xevent->u.u.detail);
}
XRecordFreeData(data);
}
void record_thread(Display* display, XRecordContext context) {
// 启用异步记录上下文
if (!XRecordEnableContext(display, context, event_callback, NULL)) {
fprintf(stderr, "无法启用记录上下文\n");
return;
}
}
int main()
{
Display *display = XOpenDisplay(NULL);
if (!display) {
fprintf(stderr, "无法打开 X 显示\n");
return -1;
}
int major = 0, minor = 0;
if (!XRecordQueryVersion(display, &major, &minor)) {
fprintf(stderr, "XRecord 扩展不可用\n");
return -1;
}
XRecordRange* range = XRecordAllocRange();
if (!range) {
fprintf(stderr, "无法分配范围\n");
return -1;
}
memset(range, 0, sizeof(XRecordRange));
range->device_events.first = KeyPress; // 4 ButtonPress
range->device_events.last = ButtonPress; // 5 ButtonRelease
XRecordClientSpec clients = XRecordAllClients;
// 创建记录上下文
XRecordContext context =
XRecordCreateContext(display, 0, &clients, 1, &range, 1);
if (!context) {
fprintf(stderr, "无法创建记录上下文\n");
XFree(range);
return -1;
}
XFree(range);
std::thread notify(&record_thread, display, context);
// 此处接收停止信号后,禁用上下文结束监控
XRecordDisableContext(display, context);
// 等待线程退出
notify.join();
// 清理
XRecordFreeContext(display, context);
XCloseDisplay(display);
return 0;
}