如何用C编写X11锁屏程序并拦截所有键盘输入?
C语言实现X11锁屏程序(拦截键盘输入)
要实现类似i3lock的锁屏程序,核心是创建全屏顶层窗口并独占键盘输入,阻止其他程序接收键盘事件,直到用户输入正确密码。以下是具体实现步骤和代码示例:
核心实现步骤
1. 创建全屏顶层窗口
- 初始化X11显示连接,获取屏幕尺寸
- 创建覆盖整个屏幕的窗口,设置为全屏状态,避免被窗口管理器移动、关闭
- 设置窗口属性,让窗口管理器将其识别为锁屏类窗口(避免被其他窗口遮挡)
2. 独占键盘输入
使用XGrabKeyboard函数抓取键盘,让所有键盘事件直接发送到锁屏窗口,其他程序无法接收键盘输入。需处理抓取失败的情况(比如已有程序占用键盘)。
3. 处理键盘输入与密码验证
- 在事件循环中拦截所有
KeyPress事件,提取输入字符 - 处理特殊按键(回车验证、退格删除、Esc清空输入)
- 使用系统密码验证机制(推荐PAM,无需root权限更安全)验证输入的密码
4. 防止非法退出
- 忽略窗口关闭事件(
WM_DELETE_WINDOW) - 处理系统信号(如SIGINT、SIGTERM),确保退出前释放键盘抓取
完整代码示例
#include <X11/Xlib.h> #include <X11/Xutil.h> #include <X11/Xatom.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pwd.h> #include <unistd.h> #include <signal.h> #include <security/pam_appl.h> static Display *global_dpy; // 生成重复字符的字符串(用于显示密码星号) char *strrep(const char *s, int n) { char *res = malloc(n + 1); if (!res) return NULL; memset(res, s[0], n); res[n] = '\0'; return res; } // PAM对话函数,用于传递输入的密码 static int pam_conv(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) { *resp = malloc(num_msg * sizeof(struct pam_response)); if (!*resp) return PAM_BUF_ERR; for (int i = 0; i < num_msg; i++) { (*resp)[i].resp = strdup((char *)appdata_ptr); (*resp)[i].resp_retcode = 0; } return PAM_SUCCESS; } // 信号处理函数,确保退出时释放资源 void sig_handler(int sig) { if (global_dpy) { XUngrabKeyboard(global_dpy, CurrentTime); XCloseDisplay(global_dpy); } exit(0); } int main() { Display *dpy; Window win; XEvent ev; Atom wm_state, fullscreen, wm_delete; char passwd[256] = {0}; int passwd_len = 0; struct passwd *pw = getpwuid(getuid()); if (!pw) { fprintf(stderr, "无法获取用户信息\n"); exit(1); } // 注册信号处理 signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler); // 打开X11显示 dpy = XOpenDisplay(NULL); if (!dpy) { fprintf(stderr, "无法打开显示\n"); exit(1); } global_dpy = dpy; int scr = DefaultScreen(dpy); Window root = RootWindow(dpy, scr); int width = DisplayWidth(dpy, scr); int height = DisplayHeight(dpy, scr); // 创建全屏窗口 win = XCreateSimpleWindow(dpy, root, 0, 0, width, height, 0, BlackPixel(dpy, scr), BlackPixel(dpy, scr)); // 设置窗口为全屏、顶层 wm_state = XInternAtom(dpy, "_NET_WM_STATE", False); fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); XChangeProperty(dpy, win, wm_state, XA_ATOM, 32, PropModeReplace, (unsigned char *)&fullscreen, 1); // 设置窗口不可关闭 wm_delete = XInternAtom(dpy, "WM_DELETE_WINDOW", False); XSetWMProtocols(dpy, win, &wm_delete, 1); // 显示窗口并获取焦点 XMapWindow(dpy, win); XSetInputFocus(dpy, win, RevertToNone, CurrentTime); XFlush(dpy); // 抓取键盘,独占所有输入 if (!XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime)) { fprintf(stderr, "无法抓取键盘,可能已有程序占用\n"); XCloseDisplay(dpy); exit(1); } // 事件循环 while (1) { XNextEvent(dpy, &ev); switch (ev.type) { case KeyPress: { char buf[32]; int len = XLookupString(&ev.xkey, buf, sizeof(buf), NULL, NULL); if (len <= 0) break; if (buf[0] == '\n' || buf[0] == '\r') { // 验证密码(使用PAM) pam_handle_t *pamh = NULL; struct pam_conv conv = { pam_conv, passwd }; int ret = pam_start("login", pw->pw_name, &conv, &pamh); if (ret == PAM_SUCCESS) { ret = pam_authenticate(pamh, 0); if (ret == PAM_SUCCESS) { // 密码正确,退出锁屏 pam_end(pamh, ret); XUngrabKeyboard(dpy, CurrentTime); XCloseDisplay(dpy); exit(0); } else { // 密码错误,清空输入 fprintf(stderr, "密码错误\n"); memset(passwd, 0, sizeof(passwd)); passwd_len = 0; } pam_end(pamh, ret); } else { fprintf(stderr, "PAM初始化失败\n"); memset(passwd, 0, sizeof(passwd)); passwd_len = 0; } } else if (buf[0] == '\b' && passwd_len > 0) { // 退格键删除最后一个字符 passwd[--passwd_len] = '\0'; } else if (buf[0] == 27) { // Esc键清空输入 memset(passwd, 0, sizeof(passwd)); passwd_len = 0; } else if (passwd_len < sizeof(passwd) - 1) { // 普通字符添加到密码 passwd[passwd_len++] = buf[0]; } // 刷新窗口,显示密码输入状态 XClearWindow(dpy, win); GC gc = XCreateGC(dpy, win, 0, NULL); XSetForeground(dpy, gc, WhitePixel(dpy, scr)); char *stars = strrep("*", passwd_len); char prompt[256]; snprintf(prompt, sizeof(prompt), "请输入密码:%s", stars ? stars : ""); XDrawString(dpy, win, gc, 50, height/2, prompt, strlen(prompt)); XFreeGC(dpy, gc); free(stars); XFlush(dpy); break; } case ClientMessage: { // 忽略窗口关闭请求 if ((Atom)ev.xclient.data.l[0] == wm_delete) { break; } // 处理全屏状态变更,确保窗口保持全屏 if ((Atom)ev.xclient.data.l[0] == wm_state) { XChangeProperty(dpy, win, wm_state, XA_ATOM, 32, PropModeReplace, (unsigned char *)&fullscreen, 1); } break; } case ConfigureNotify: { // 窗口大小变更时重新设置为全屏 width = ev.xconfigure.width; height = ev.xconfigure.height; XResizeWindow(dpy, win, width, height); break; } default: // 忽略其他事件 break; } } return 0; }
编译与运行
编译时需要链接X11、PAM和crypt库:
gcc lock.c -o xlock -lX11 -lpam -lcrypt
运行程序后,输入用户密码回车即可解锁;Esc清空输入,退格键删除字符。
注意事项
- 多屏幕支持:上述代码仅处理主屏幕,若需支持多屏幕,需遍历所有屏幕创建对应窗口或创建覆盖整个虚拟桌面的窗口。
- TTY切换拦截:X11程序无法拦截Ctrl+Alt+F1这类TTY切换快捷键,若需阻止,需修改系统配置(如禁用TTY切换),但这通常不推荐。
- 权限问题:使用PAM验证无需root权限;若直接读取
/etc/shadow,程序需以root权限运行或配置特殊权限,安全性较低。 - 窗口管理器兼容性:部分窗口管理器可能需要额外设置窗口属性(如
_NET_WM_WINDOW_TYPE_SPLASH)来确保窗口始终在顶层。
内容的提问来源于stack exchange,提问作者h00die
相关产品推荐
相关产品推荐

