资讯动态

C语言实现Linux屏幕取词翻译工具:基于X11 Selection的轻量方案

发布时间:2026/10/23 10:16:39 来源:尧图企业网站定制
简介一个基于C语言实现的Linux屏幕取词翻译工具源码包面向Linux平台开发者与C语言学习者。程序可捕捉终端或桌面文本选区调用翻译接口即时显示释义适合需要快速查阅外文文档或希望在Linux下开发同类工具的人群。源码涵盖取词检测、用户界面、配置管理等模块并对Bing Translate与Google Translate两套API的接入均有实现便于对比不同翻译服务的集成方式。包体共141个文件大小16.7MB。以C源文件40个与头文件25个为核心配合Makefile工程文件、UI界面定义、shell辅助脚本、GIF演示图及PNG资源图结构清晰。既有可编译运行的完整工程也有界面与配置模块可独立阅读适合中高级C程序员深入分析。目前已有124人学习浏览。通过阅读代码可掌握Linux屏幕取词与鼠标事件捕获、外部翻译API调用、多模块协作与配置文件解析等技巧项目中还保留了Bug记录与测试文件对理解真实项目的迭代过程也有参考价值。1. 屏幕取词翻译工具一份可直接编译的 C 语言实现解决的却是日常最烦人的事做 Linux 下 C 语言软件/插件开发的人应该都有过这种经历读一篇英文技术文档、看一份英文 README光标停在某个生词上却懒得复制、切窗口、粘贴到网页翻译。屏幕取词翻译这个需求本身不复杂复杂的是在 Linux 上让它不抢焦点、不卡死、不依赖一大堆 Python 环境地长期驻留。这份基于 C 语言实现的 Linux 屏幕取词翻译源码正好把整条链路做成了一个小而完整的 C 工程X11 取词、调用在线翻译接口、弹窗展示译文。适合两类人——只想拿到一个能编译能用的轻量取词器的日常用户以及想研究 X11 选择机制和事件循环怎么协作的 C 开发者。2. X11 取词与翻译链路为什么选 C、为什么走 Selection 而不是 OCR2.1 为什么不用 Python 脚本而用 C 做常驻工具屏幕取词类工具往往被误认为“用 Python 写个脚本就行”。在功能演示层面确实可以但落到真实桌面环境里差异就出来了。Python 方案通常要带上 pyautogui模拟鼠标、pyperclip读取剪贴板、requests请求翻译接口三件套再配一个 GUI 库做悬浮窗。这还没算 Python 运行时本身。启动速度上解释器加载加模块导入轻松几百毫秒而 C 版本静态编译后就是一个几 MB 的二进制XOpenDisplay完成后立刻进入事件循环。常驻内存也差得远Python 脚本常驻时 RSS 很容易跑到 30MB 以上C 实现可以压到 10MB 以内。更重要的是 X11 客户端开发。XConvertSelection、XGetWindowProperty、SelectionNotify这些接口本来就是 Xlib 的原生能力用 C 直接调是零包装。Python 里要用 python-xlib 这类第三方库去包一层文档少、类型转换繁琐、多线程里还容易出 GIL 问题。对比项C 版本Python 脚本方案编译产物单一二进制约几 MB依赖解释器与多个 pip 包启动耗时毫秒级即时响应百毫秒级首次调用偏慢X11 API 支持Xlib 原生调用依赖第三方绑定库常驻内存约 10MB 内容易超过 30MB调试手段gdb 直接跟需要额外工具链除此之外这份源码的模块边界清楚取词模块只管 X11 通信翻译模块只管 HTTP 与 JSON窗口模块只管渲染。对想学 C 工程组织的人来说它是一个比“Hello World”有营养、又不会复杂到读不懂的项目。2.2 取词原理X11 的三种 Selection 与 UTF8_STRING 目标Linux 桌面上的“取词”不靠截屏识别靠的是 X11 的 Selection选择机制。X11 里有三种内建选择PRIMARY、CLIPBOARD、SECONDARY。PRIMARY鼠标划选后焦点窗口把选中文本放进去。CLIPBOARD显式执行“复制”操作后文本进入剪贴板。SECONDARY在实际应用中基本没人用。大多数图形终端、PDF 阅读器、浏览器在鼠标划选时都会把文本写进PRIMARY。所以取词工具的正确做法不是截屏而是向系统请求PRIMARY选择的内容。但这里有个关键细节XConvertSelection是异步请求。调用后事件循环会收到一个SelectionNotify事件真正的数据要通过XGetWindowProperty从这个事件携带的窗口属性里读出来。初次接触的人很容易犯一个错误——调用完XConvertSelection后立刻去读属性结果读到空值。正确顺序是先XConvertSelection发起请求再回到XNextEvent等待事件到达在事件分支里做读取。还有编码问题。现代应用往选择里写入的 target 通常叫UTF8_STRING但老程序可能只支持STRING或TEXT。因此请求时要显式构造一个UTF8_STRING的 Atom 作为转换目标取回后最好做一次 UTF-8 合法性校验。遇到不支持UTF8_STRING的窗口常见的兜底策略是改用STRING再按ISO-8859-1转回 UTF-8否则拿到乱码。Atom utf8_string XInternAtom(dpy, UTF8_STRING, False); // 请求把 PRIMARY 选择的内容转成 UTF8_STRING XConvertSelection(dpy, XA_PRIMARY, utf8_string, XA_STRING, sel_win, CurrentTime); XFlush(dpy); // 然后回到事件循环等待 SelectionNotify这段代码里XA_PRIMARY是取词来源utf8_string是期望的目标格式sel_win是程序自己创建的一个隐藏窗口用它的属性来接收返回的数据。第四个参数XA_STRING不是格式而是数据存放的位置——表示结果会挂在sel_win窗口的XA_STRING属性上。CurrentTime表示不指定时间戳直接取当前选择内容。2.3 翻译与展示请求要超时、弹窗别抢焦点取到文本后下一步是调用翻译接口。常见做法是用 libcurl 发 POST 请求把来源语言、目标语言、待翻译文本放进 JSON body再把响应 JSON 解析出来。C 语言里没有内建 JSON 解析工程里集成 cJSON 这种单文件库就够了几百行代码不引入重量级依赖。这里要注意两件事。第一网络请求不能在 X11 事件循环主线程里同步执行。一次翻译接口往返通常要几百毫秒到几秒同步阻塞会让整个取词器“假死”鼠标怎么点都没反应。常见做法是开一个 pthread 工作线程把翻译结果通过队列传回主线程。第二必须给 libcurl 设置超时。我一般会把CURLOPT_TIMEOUT_MS设成 8000连接超时设成 3000避免网络抖动时取词器长时间没反应。展示层是最后一个问题。弹窗窗口如果走标准窗口管理器流程会拿到焦点、出现在任务栏这会把用户正在操作的输入焦点抢走。正确姿势是创建一个override_redirect属性的窗口相当于告诉 X Server这个窗口不交给窗口管理器管理。窗口直接按坐标出现在屏幕上不会抢焦点、不会进任务栏点击外部区域时自动消失需要关闭时自己处理按键事件。XSetWindowAttributes attrs; attrs.override_redirect True; // 不进窗口管理器 attrs.background_pixel 0x202020; // 深色底 Window win XCreateWindow(dpy, root, x, y, w, h, 0, CopyFromParent, InputOutput, CopyFromParent, CWOverrideRedirect | CWBackPixel, attrs); XMapRaised(dpy, win); // 直接置顶显示这段代码里x、y是弹窗的左上角坐标通常取鼠标当前位置加一个偏移避免窗口盖住正在阅读的文本。CWOverrideRedirect让窗口脱离窗口管理器控制XMapRaised把它放到堆叠顺序最上层。弹窗要做的交互其实很少鼠标移出窗口区域或按下 Esc 就销毁窗口回到XNextEvent继续等下一个取词请求。2.4 为什么默认不集成 OCR很多人会把“屏幕取词”和“截屏识别文字”混为一谈。屏幕取词针对的是可选中文字——终端、浏览器、编辑器里的英文单词OCR 针对的是图片里的文字。两者是不同场景。这份源码走的是“选中文字 → 翻译 → 弹窗”的路线不把 OCR 放进主链路。原因很直接OCR 需要集成 Tesseract 或类似引擎要下语言包识别中文或代码片段时错误率不稳定而且对高 DPI 截图还要做预处理。把它放进主循环会让工具重好几倍启动速度、常驻内存的优势都会被拖垮。如果确实需要识别图片里的文字正确做法是作为独立扩展模块调用而不是混进取词主流程。用XGetImage抓取鼠标周围区域、交给外部 OCR 进程、拿到文本再走翻译链路。这份源码留出的翻译接口可以复用只是输入源从 Selection 变成文本文件。对于大多数人处理文档场景时选中文本远比 OCR 可靠。3. 源码工程拆解从取词到弹窗译文的完整实现3.1 工程结构与构建方式拿到这份源码压缩包后解压出来是一个标准的 C 工程目录。先看文件布局大概长这样文件/目录作用关键依赖src/main.c主入口初始化显示连接、注册热键、进入事件循环X11/Xlib.hsrc/xgettext.c取词模块请求 PRIMARY 选择、解析 SelectionNotifyX11/Xutil.hsrc/translate.c翻译模块拼 JSON、请求接口、解析结果libcurl、cJSONsrc/osd.c悬浮窗模块创建 override-redirect 窗口、绘制多行文本X11/Xft.hsrc/config.ini可配置项翻译接口地址、语言参数、字体名无build.sh构建脚本统一编译参数与链接库pkg-config构建前要确认几个开发库已经装好。在基于 Debian 的桌面环境上常见做法是一条命令装齐sudo apt-get install -y libx11-dev libxft-dev libxext-dev \ libcurl4-openssl-dev libcjson-dev pkg-config如果你用的发行版没有 libcjson-dev 这个包也可以直接把 cJSON 的.c和.h文件放进src/参与编译。装完依赖后执行构建脚本#!/bin/sh set -e CFLAGS-Wall -O2 -g $(pkg-config --cflags x11 xft xext) -I./src LIBS$(pkg-config --libs x11 xft xext) -lcurl -lcjson -lpthread gcc $CFLAGS \ src/main.c \ src/xgettext.c \ src/translate.c \ src/osd.c \ $LIBS \ -o screen-dict echo build ok: ./screen-dict这里有一点值得说明pkg-config --cflags会展开成-I/usr/include/X11这类头文件路径--libs会展开成-lX11 -lXft -lXext这类链接参数。如果你的系统里 X11 头文件路径不同比如安装在/usr/local/include需要手动在CFLAGS里补-I路径。3.2 取词模块读取 PRIMARY 选择的正确时序取词模块是整条链路里最值得细读的部分。它的核心工作流程是先创建一个隐藏窗口接收选择通知再注册热键监听用户选中文本后按下触发键程序执行XConvertSelection解析返回的SelectionNotify事件最终得到字符串。以下这段代码是从工程里提炼出的关键路径处理SelectionNotify事件并读取返回数据static char *read_selection_text(Display *dpy, Window sel_win) { Atom utf8_string XInternAtom(dpy, UTF8_STRING, False); Atom target_prop XInternAtom(dpy, XSEL_DATA, False); Atom xsel_data; // 检查属性是否真正被写入避免读到上一次的残留数据 int status XGetWindowProperty(dpy, sel_win, target_prop, 0, 1024, False, AnyPropertyType, xsel_data, fmt, nitems, after, prop); if (status ! Success || xsel_data None) return NULL; // 从属性里取出实际字节按 UTF-8 解出字符串 unsigned char *data ...; // 从 prop 的 value 字段拷贝 return (char *)data; }取词时序上最容易翻车的点是SelectionNotify事件到达后数据并不一定马上就绪。XConvertSelection请求发出去后需要目标窗口响应这个过程是异步的。如果事件到了但属性还没写入要稍等片刻重试或者对XGetWindowProperty返回的after字段做判断超过一定长度时循环读取。工程里对这个问题给出的处理是在事件分支里拿到SelectionNotify后先用XGetWindowProperty查询属性是否存在不存在就XFlush后短暂等待重试。这样虽然会引入几十毫秒延迟但稳定性比“立刻读”高很多。3.3 翻译模块libcurl 请求与 JSON 解析翻译模块是独立的它不知道文字是从哪里来的只负责“输入字符串输出译文字符串”。这样设计的好处是将来如果要把输入源从 Screen Selection 换成 OCR 文本翻译模块不需要改动。char *translate_text(const char *text) { CURL *curl curl_easy_init(); if (!curl) return NULL; // 组装 JSON bodyq 是原文source/target 是语言代码 cJSON *body cJSON_CreateObject(); cJSON_AddStringToObject(body, q, text); cJSON_AddStringToObject(body, source, auto); cJSON_AddStringToObject(body, target, zh); char *json_str cJSON_PrintUnformatted(body); // 用 XTranslation API 的地址实际使用时可改 config.ini curl_easy_setopt(curl, CURLOPT_URL, http://api.x-translate.local/v1/translate); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_str); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 8000L); // 总超时 8 秒 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L); // 连接超时 3 秒 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); CURLcode res curl_easy_perform(curl); if (res ! CURLE_OK) { fprintf(stderr, translate request failed: %s\n, curl_easy_strerror(res)); curl_easy_cleanup(curl); cJSON_Delete(body); free(json_str); return NULL; } // 解析响应 JSON读取 translation 字段 cJSON *resp cJSON_Parse(response_buffer); char *result strdup(cJSON_GetObjectItem(resp, translation)-valuestring); cJSON_Delete(resp); cJSON_Delete(body); free(json_str); curl_easy_cleanup(curl); return result; }几个参数值得停下来说。CURLOPT_POSTFIELDS传的是整个 JSON 字符串所以要在外面用cJSON_PrintUnformatted生成紧凑格式不能带多余换行否则接口可能拒收。CURLOPT_TIMEOUT_MS是整个请求的总时间上限包含连接、发送、等待响应、接收全部数据的时间CURLOPT_CONNECTTIMEOUT_MS只限制 TCP 建连阶段。两者都设可以避免“连上了但响应很慢”导致的长时间卡顿。write_cb是 libcurl 写回调把响应体累积到一个内存缓冲区工程里给了默认实现。解析响应时只用cJSON_GetObjectItem拿translation字段没有做复杂的错误分支处理。如果接口返回非 JSON 或缺少字段cJSON_Parse或GetObjectItem会返回 NULL调用方要能容忍空指针直接提示“翻译失败”。3.4 悬浮窗模块override-redirect 窗口与中文渲染拿到译文后展示窗口要做得克制不抢焦点、不挡视线、看得清中文。这里的关键技术点有两个override-redirect 窗口创建、Xft 字体渲染。窗口创建在 2.3 节已经贴过代码这里补充字体渲染部分。X11 原生接口不认中文要渲染中文就必须走 Xft 方案。实现步骤是先用XftFontOpenName打开系统中文字体再用XftDrawCreate创建绘制上下文最后逐行调用XftDrawStringUtf8输出文本。XftFont *font XftFontOpenName(dpy, screen, Noto Sans CJK SC:size14); XftDraw *draw XftDrawCreate(dpy, win, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen)); XftColor color; XftColorAllocName(dpy, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen), white, color); // 每行文本 y 坐标递增渲染多行译文 for (int i 0; i line_count; i) { XftDrawStringUtf8(draw, color, font, 12, 18 i * 22, (XftChar8 *)lines[i], strlen(lines[i])); }这里要注意字体名的写法。XftFontOpenName接受的是 Fontconfig 模式字符串Noto Sans CJK SC:size14表示用该字体、字号 14。如果系统里装的是文泉驿正黑就改成WenQuanYi Zen Hei:size14。字体不存在时XftFontOpenName返回 NULL程序要自动退回Sans:size14至少保证英文能显示。提示中文显示成方块豆腐块几乎都是字体问题。先确认fc-list | grep -i cjk有没有输出再确认字体名字写对了这两个检查能解决大多数渲染异常。4. 常见问题与避坑排查乱码、Wayland 失效与坐标偏移4.1 取回的文本全是乱码现象划选英文单词后触发取词弹窗里看到的是é这类乱码或者只有一两个字符。原因目标窗口不支持UTF8_STRING这个 targetX Server 只能退回STRING编码也就是 ISO-8859-1非 ASCII 字符被强行按单字节解释中文和重音字符全变成乱码另一种常见情况是请求后没有等SelectionNotify就抢读属性读到的数据是不完整的。解决先检查目标程序类型。xterm、urxvt这类老终端通常不支持UTF8_STRING要改用XInternAtom(dpy, STRING, False)再取取到后按 Latin-1 手工转 UTF-8。另外在取词模块的SelectionNotify分支里加一个短重试逻辑属性为空时usleep(20000)再读一次能解决不少偶发乱码。4.2 在 Wayland 桌面环境下完全失效现象源码在 X11 桌面例如 Xfce、i3下一切正常切到默认 Wayland 的桌面环境后弹窗要么不出现要么取词结果为空系统日志里报BadAccess或BadWindow。原因Wayland 合成器不允许客户端像 X11 那样随意访问全局选择内容、读取任意窗口的坐标。XConvertSelection在 Wayland 的 XWayland 兼容层里权限被限制取词请求直接被忽略。解决最稳妥的做法是让这个工具运行在 X11 会话下。登录界面选择“Xorg”或“X11”会话模式而不是默认的 Wayland。如果你必须在 Wayland 环境使用可以整个程序跑在 XWayland 里但取词范围仍然受限只有在 XWayland 窗口内划选才有响应。原生支持 Wayland 需要改用 GTK 的 Wayland 接口重新实现一遍取词工程量大且不值得。4.3 高分屏下弹窗位置错位现象在 2K 或 4K 屏幕上缩放比例设为 150% 时弹窗出现在鼠标斜上方很远的位置而且字体偏小。原因X11 的鼠标坐标和窗口坐标用的是逻辑坐标而高 DPI 环境下系统通过Xft.dpi做了全局缩放。直接拿鼠标坐标创建窗口显示出来的实际位置会按缩放因子偏掉。也就是说鼠标在屏幕中央弹窗却出现在左上角附近。解决创建弹窗前读取当前Xft.dpi值计算缩放比scale dpi / 96把x、y、窗口宽高、字体大小全部乘上这个系数。注意不要只乘坐标不乘字号否则窗口位置对了、字却小得看不清。// 读取系统 DPI 作为缩放基准 double scale 1.0; char *dpi_str XGetDefault(dpy, Xft, dpi); if (dpi_str) { int dpi atoi(dpi_str); if (dpi 96) scale (double)dpi / 96.0; } // 所有窗口参数都乘以 scale win_x mouse_x 16 * scale; win_y mouse_y 20 * scale; win_w 360 * scale;4.4 程序启动前已选中的文本取不到现象先在某处划选了一段英文再启动取词器按热键没有反应但启动后再划选一切正常。原因取词器的隐藏窗口是在程序启动时才创建的它只能接收到启动之后的SelectionNotify事件。启动前选中的文本其选择持有者XGetSelectionOwner返回的窗口是别的程序取词器根本没有拿到相关通知。解决把取词器做成常驻进程开机自启或用你的桌面启动器拉起而不是要用时才启动。另外取词前用XGetSelectionOwner先检查PRIMARY是否有人持有如果返回None直接提示“未选中文本”比无响应友好得多。4.5 翻译接口响应慢导致取词器假死现象网络正常时一切正常一旦接口响应超过几秒整个取词器鼠标点击、键盘输入全部没反应过一会儿自己恢复。原因翻译调用是同步执行的curl_easy_perform阻塞在主线程里而这里的主线程同时负责XNextEvent事件分发。事件循环被卡住所有 X11 事件都无法处理界面自然就“假死”了。解决翻译逻辑必须放进独立线程。工程里的做法是主线程收到取词请求后把文本塞进一个任务队列工作线程从队列取文本调用翻译接口再把结果放回结果队列主线程每次回到XNextEvent前检查结果队列有结果才更新弹窗。这样即使接口 8 秒超时取词器始终能响应鼠标移动和按键用户体验上只是翻译结果晚到程序不会卡死。5. 进阶与验证接入系统通知并自查整条链路5.1 把译文交给系统通知替代自绘悬浮窗自绘悬浮窗最大的短板是字体和坐标都得管而且只在一个屏幕上生效。如果你的使用场景是“翻译完瞄一眼就走”完全可以绕开窗口模块直接把译文交给系统的通知守护进程也就是用notify-send输出。好处是通知的样式、多显示器位置、字体渲染全部由桌面环境负责代码还能少掉大半。# 手动测试通知接口是否可用 notify-send 取词翻译 Today is a good day. --expire-time5000工程里预留了一个osd_mode配置项值改成notify时translate.c拿到译文后不再调用osd.c的绘制函数而是 fork 出子进程执行notify-send。我一般会把--expire-time设成 5000 毫秒太短来不及看完太长会挡住桌面。5.2 使用 xclip 做端到端验证改完代码后怎么确认取词链路是通的手工划选调试太慢我习惯直接用xclip往PRIMARY里塞一段文本模拟划选。xclip 的作用是把标准输入写入指定选择正好能模拟“用户在任意程序里选中了一行字”的状态。# 写入 PRIMARY 选择模拟划选 echo linux c language | xclip -selection primary # 触发取词器热键观察弹窗或通知输出的译文这条命令的好处在于可重复、无需图形交互。配合strace还能进一步确认取词流程strace -e traceread,write ./screen-dict 21 | grep X11能看到 X11 协议层的读写时序。每次改完代码我先用 xclip 造数据再触发热键最后看译文输出三步下来基本能把取词、翻译、展示三块问题分开定位。5.3 增加一个本地缓存减少重复请求屏幕取词工具最容易被嫌弃的细节就是“同一个词翻一遍查一遍”。给translate.c加一个简单的历史缓存用临时文件存键值对查词前先读缓存命中就直接回显译文不发起 HTTP 请求。实现上不需要 SQLite一个文本文件加哈希表就够了。# 缓存文件格式每行一个词条 # wordTABtranslation缓存命中时打印一行日志没命中才走网络请求。这样高频取词时既省流量又省时间而且接口偶尔抽风时常用词的翻译仍然能正常显示。这个增强代码量不大但对日常使用体验的提升非常明显。从那以后我每次在 Linux 上折腾这类常驻小工具都会强制走一遍“依赖检查、构建、xclip 模拟、日志验证”这四个步骤顺序反了必翻车。这份取词翻译源码也是这么验证下来的X11 的坑绕过了剩下就是怎么按你的桌面环境去调字体和弹窗位置的问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑