简介:屏幕取词在线翻译工具是一款面向多语言工作者、外语学习者及科研人员的轻量辅助软件,能有效解决阅读外文网页、PDF文献或处理国际邮件时反复复制粘贴翻译的痛点。资源内含QTranslate主程序及配套服务配置,共72个文件,以txt配置、json服务参数、js扩展脚本、exe可执行程序等类型为主,压缩包仅330KB,免安装、易携带,适合日常随取随用。工具集成Google、Microsoft Translator等多家翻译引擎,支持屏幕取词、PDF文本识别、快捷键唤起、后台发音等功能,用户可按需切换或自定义翻译服务,兼顾准确性与效率。目前已有80人学习下载,目录结构清晰,既可直接部署使用,也有助于研究多翻译引擎的接入与配置思路。
1. 屏幕取词在线翻译工具:为什么划词翻译解决不了读文档的痛点
屏幕取词在线翻译工具,最常见的真实场景是你打开一篇英文 PDF、在 IDE 里读注释、或者对着一个弹窗报错信息发愣——鼠标停在一个生词上,想翻译,又不想切走当前窗口。划词翻译解决不了这个场景,因为它要求你先选中文本;桌面级的屏幕取词得自己拿鼠标坐标、自己取文本、再自己弹窗展示结果。这个工程包的核心是一条取词管道:低级鼠标钩子拿坐标、UI Automation 优先取文本、OCR 兜底处理扫描 PDF 和图片字幕,最后把命中的文本送去在线翻译引擎,在一个不抢焦点的悬浮窗里展示译文。适合谁?常读外文文档的程序员、需要边看边译的研究者,以及想亲手做一个桌面取词工具的人。下面从选型开始,把它拆成能落地的工程细节。
2. 取词路径选型:为什么先用低级鼠标钩子而不是全局键盘钩子
屏幕取词的第一步不是“取词”,而是“取坐标”。你要先知道鼠标停在哪,才能决定去哪个控件里找文本。这里有两个容易踩的误判:一是以为钩子能直接拿到单词文本,二是以为取词就是不停读剪贴板。实际工程里这三个动作是拆开的,优先级也不同。
2.1 低级鼠标钩子:WH_MOUSE_LL 只拿坐标,不负责取词内容
Windows 上做全局鼠标监听,常见做法是用WH_MOUSE_LL低级鼠标钩子。它的特点是回调运行在安装线程自己的消息循环里,不需要把代码注入到别的进程,这比WH_MOUSE老式钩子省掉了一大堆 DllMain 和 32/64 位注入的麻烦。工程包里的核心注册代码是这样的:
private const int WH_MOUSE_LL = 14; private static HookProc _proc = HookCallback; private IntPtr _hookId = IntPtr.Zero; public void InstallHook() { using (Process curProcess = Process.GetCurrentProcess()) using (ProcessModule curModule = curProcess.MainModule) { _hookId = SetWindowsHookEx(WH_MOUSE_LL, _proc, GetModuleHandle(curModule.ModuleName), 0); } } private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { // MSLLHOOKSTRUCT 里只有坐标、滚轮、时间戳,没有文本 var data = Marshal.PtrToStructure<MSLLHOOKSTRUCT>(lParam); _latestPoint = data.pt; _latestTime = DateTime.UtcNow; } return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); }有两个参数值得留意。第一个是WH_MOUSE_LL = 14,它是低级钩子常量,回调里能收到鼠标移动、按下、滚轮等事件;wParam是事件类型,比如0x0200是鼠标移动,0x0201是左键按下,实际取词我只关心移动事件。第二个是SetWindowsHookEx的最后一个线程参数0,表示监听所有线程的鼠标事件。
回调里拿到MSLLHOOKSTRUCT后,里面只有pt.x、pt.y坐标、mouseData、flags和时间戳,没有任何文本信息。这一点必须想清楚:钩子只是“哨兵”,真正的取词动作在拿到坐标之后才发生。另外,回调函数要尽量轻,不要在回调里发网络请求,因为低级钩子会影响整个系统鼠标事件的分发,回调里一旦有阻塞操作,所有程序的鼠标都会卡顿。工程里的做法是回调只更新坐标和时间,然后由后台线程去检查是否该触发取词。
2.2 文本来源优先级:UI Automation 优先、剪贴板兜底、OCR 最后
拿到屏幕坐标之后,怎么把坐标变成单词?Windows 自带的 UI Automation(UIA)是第一选择。通过AutomationElement.FromPoint可以直接拿到鼠标位置处的界面元素,然后读Current.Name属性,很多原生控件和部分 Electron 应用的 Name 就是可见文本。
AutomationElement element = AutomationElement.FromPoint(point); string text = element?.Current.Name; // 有些控件 Name 为空但父节点有文本,向上找一层 if (string.IsNullOrEmpty(text) && element != null) { var parent = TreeWalker.ControlViewWalker.GetParent(element); text = parent?.Current.Name; }这段逻辑对 Win32 窗口、WPF、部分 Qt 应用都有效。但两个典型场景会失效:一是 Chrome、Firefox 这类浏览器,网页内部的文本不经 UIA 暴露,FromPoint拿到的元素 Name 往往是空的;二是图片、游戏画面这类非文本渲染。所以工程里把 UIA 作为第一优先级,但不把它当唯一路径。
兜底方案是剪贴板监听。浏览器里用户其实习惯了“划一下再 Ctrl+C”,真正省心的是鼠标划选后自动复制。工具会在鼠标静止并检测到划选动作时模拟发送Ctrl+C,然后监听剪贴板变化拿到文本。这个方案的缺点是会破坏用户自己的剪贴板内容,所以工程里实现了一个剪贴板栈:取词前保存当前剪贴板内容,取词后若内容被替换则延时恢复。这个方案虽然“打扰”,但在浏览器场景里的命中率最高。
2.3 什么时候必须走 OCR:扫描 PDF、图片字幕与不可访问控件
有些文本既不在 UIA 里,也不能靠 Ctrl+C 拿到——比如 PDF 扫描件、视频字幕、游戏内英文。这时候必须 OCR。工程里优先调用 Windows 10 自带的Windows.Media.Ocr,它离线可用,英文识别速度在 100ms 到 300ms 之间,不需要额外装引擎;中文识别率一般,但屏幕取词场景大多是英文文档,足够用。
// 创建英文识别引擎,Win10 1709+ 可用 OcrEngine ocr = OcrEngine.TryCreateFromLanguage( new Windows.Globalization.Language("en-US")); // 从屏幕区域截取位图,建议转成灰度后识别率更高 using (var stream = CaptureRegionToPng(region)) { var decoder = await BitmapDecoder.CreateAsync(stream); SoftwareBitmap bitmap = await decoder.GetSoftwareBitmapAsync(); OcrResult result = await ocr.RecognizeAsync(bitmap); return result.Text; }OCR 有几个真实参数要注意。截取区域大小:宽 400 到 600 像素、高 100 像素左右即可,太大的区域会引入大量噪声;识别引擎对白底黑字文本最友好,深色背景要先把位图反色再送进去;RecognizeAsync是异步方法,OCR 期间界面线程不能锁死。识别完成后得到的是一整块文本,需要按空格和换行切词,跟鼠标落点做最近词匹配。这三个取词路径的优先级在工程包里是可以配置的:默认UIA > 剪贴板 > OCR,在图片类窗口下可以手动切换成OCR 优先。
3. 把工具搭起来:热键、悬浮窗与取词管道的主流程
选型定了之后,工具的整体骨架就清晰了:一个隐藏的主进程负责注册钩子和热键,一个悬浮窗负责展示译文,中间用队列把取词、翻译、渲染串起来。这一章讲工程里实际怎么搭这三个模块,参数抄下来就能跑。
3.1 全局热键与隐藏主窗口:RegisterHotKey 的正确姿势
有些时候鼠标取词不触发,比如鼠标静止但不想取词的区域;这时候需要一个手动开关。工程里默认用Alt+Q作为翻译快捷键,RegisterHotKey注册的是系统级热键,不需要窗口在前台也能响应。
const int HOTKEY_ID = 0x9001; const uint MOD_ALT = 0x0001; const uint MOD_NOREPEAT = 0x4000; const uint VK_Q = 0x51; RegisterHotKey(_hwnd, HOTKEY_ID, MOD_ALT | MOD_NOREPEAT, VK_Q);参数里MOD_ALT表示 Alt 键,VK_Q是 Q 的虚拟键码,MOD_NOREPEAT这个参数很重要——不加它,按住 Alt 时会连续触发多次热键,翻译弹窗会一闪一闪。注册成功后主窗口不能销毁,热键消息是发给窗口过程的,所以工程里主窗口做了特殊处理:窗口大小设成 0、不显示在任务栏、ShowInTaskbar=false,但窗口句柄必须一直活着,消息循环不能退出。
消息循环和钩子线程的关系也容易搞错。WH_MOUSE_LL钩子的回调需要所在线程有消息循环,否则回调根本不会被调用。工程里这段逻辑放在一个专用线程里,线程启动时调用Application.Run()或GetMessage()循环,不能放在线程池线程上随便跑。我见过不少人把钩子装在 UI 线程但 UI 线程又被await阻塞,结果钩子时灵时不灵,排查时先检查有没有消息循环。
3.2 悬浮窗 UI:不抢焦点、置顶、自动隐藏的窗口参数
取词弹窗最忌讳的是抢焦点。想象一下你在 IDE 里写着代码,鼠标停在一个方法名上,弹窗弹出后光标焦点没了,这工具就没法用了。所以悬浮窗的窗口样式是这套工程里最抠细节的部分:
// 取词结果窗,原则:不抢焦点、不占任务栏、始终置顶 WindowStyle = WindowStyle.None; ShowInTaskbar = false; Topmost = true; ResizeMode = ResizeMode.NoResize; // Win32 层再强制一层 var exStyle = GetWindowLong(_hwnd, GWL_EXSTYLE); SetWindowLong(_hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE);WS_EX_TOOLWINDOW让窗口不出现任务栏和 Alt+Tab 列表;WS_EX_NOACTIVATE是关键中的关键,它告诉系统这个窗口永远不激活,点击它也不会抢走当前窗口焦点。窗口内部放一个只读的 TextBlock,背景色用半透明、字体大小默认 14pt,这些都可以在配置里调。
显示和隐藏策略也影响体验。工程里默认:鼠标静止超过 300ms 且当前窗口不是输入框时显示弹窗,鼠标移动超过 50 像素后隐藏,如果连续翻译同一个词则不重复弹窗。弹窗位置默认跟随鼠标,在鼠标右下角偏移 12 像素;如果鼠标靠近屏幕右边缘,就翻转到左侧显示。这部分逻辑叫“弹窗翻转”,很多初版工具没做,导致鼠标在屏幕右侧时译文弹在屏幕外面,看起来就像没触发。
3.3 主流程:静止计时、去抖与阶段状态机
取词过程不适合用顺序代码写死,因为鼠标在动、翻译请求在飞、用户可能在打字。工程里把这套流程拆成一个简单的状态机,四个状态:WaitMouse→Debounce→FetchText→Render。
第一步WaitMouse状态里,后台线程每隔 50ms 读一次鼠标坐标;坐标连续三次都没有变化,说明用户可能真的停住了,进入Debounce状态。Debounce是去抖等待,一般等待 250ms 到 300ms,目的不是响应快,而是确认用户不是在鼠标移动的间隙里“误停”。去抖期间鼠标一旦移动超过 3 个像素,立刻重置回WaitMouse。这个 250ms 和 3px 两个参数是调出来的:太短会频繁弹出,太长又显得迟钝。工程里把它们暴露在配置文件里,我一般按人习惯设成 300ms / 4px。
进入FetchText后才真正执行上一章说的取词管道:UIA 优先,拿不到就尝试剪贴板,再不行就 OCR。取到文本后先进本地缓存查一遍,有就直接Render,没有才发翻译请求。整个流程里翻译请求是异步的,UI 不会被阻塞。如果用户在翻译请求返回前已经移动鼠标离开了,那么结果回来时直接丢弃,不弹窗。这个“结果过期”的判断逻辑用的是一个自增版本号,每次鼠标静止都会+1,异步回调回来时对比版本号,不一致就丢弃。版本号方案比取消任务简单得多,也稳得多,后面调延迟时还会再提到它。
4. 在线翻译引擎接入:签名、错误码与缓存降级
取词管道拿到文本后,最核心的外部依赖就是在线翻译接口。这一章专门讲引擎怎么选、请求怎么签名、错误怎么兜底。屏幕取词场景对延迟敏感,翻译请求必须在 300ms 内返回才算合格,否则取词体验就跟不上鼠标操作了。
4.1 引擎选型:延迟、额度与离线兜底的权衡
国内能稳定调用的翻译引擎不多,工程包里默认适配了百度翻译、有道智云、腾讯云三家。三家的核心差异不在译文质量上,而在免费额度和 QPS 限制。屏幕取词工具的特性是请求频次高、单次文本短,所以 QPS 比总额度更关键。
| 引擎 | 免费额度 | QPS 限制 | 单次文本上限 | 备注 |
|---|---|---|---|---|
| 百度翻译标准版 | 每月 100 万字符 | 1 QPS | 6000 字符 | 个人够用,签名简单 |
| 有道智云 | 每月 50 万字符 | 50 QPS | 5000 字符 | 并发友好,SDK 重 |
| 腾讯云机器翻译 | 每月 500 万字符 | 5 QPS | 5000 字符 | 免费额度大,但需实名 |
百度标准版 QPS 只有 1,意味着同一秒内只能发一个请求。快速滑过一屏连续取词时很容易踩 54003 限流,但它的签名最简单,所以工程里默认拿它做首选引擎,加一层本地缓存后 1 QPS 基本够用。如果你习惯快速连续取词,建议把默认引擎切到有道,50 QPS 短时间足够。
另外要明确一个边界:在线翻译引擎依赖网络,断网时工具会退化成“词典模式”——只显示本地的词形和词频,不显示译文。工程包里内置了一份高频词表,覆盖四六级、考研和编程常用词约 3000 个,断网时至少能给出单词原形和常见释义,不至于完全瘫掉。这个设计在线下演示时很加分,实际使用也不鸡肋。
4.2 百度翻译签名与请求:salt、MD5、URL 编码
百度翻译新版接口的签名规则是文档里最容易被误解的部分。签名串的拼接顺序是appid + 原文 + salt + 密钥,注意这里是“原文”,不是 URL 编码后的文本,也不是 JSON 序列化后的文本。网上不少旧教程用了appid+q+salt少了密钥,或者在拼接前把 q 做了编码,结果就是一直报54000 INVALID_SIGN。
import hashlib import random import requests APPID = "2024xxxxxxxx" SECRET_KEY = "xxxxx" def translate(q): # salt 每次请求都要不同,建议用随机数,不要用固定值 salt = str(random.randint(32768, 65536)) # 签名原文:appid + 原始文本 + salt + 密钥 raw = APPID + q + salt + SECRET_KEY sign = hashlib.md5(raw.encode("utf-8")).hexdigest() resp = requests.post( "https://api.fanyi.baidu.com/api/trans/vip/translate", data={ "q": q, "from": "auto", "to": "zh", "appid": APPID, "salt": salt, "sign": sign, }, timeout=2.0, # 取词场景 2 秒必须返回,否则宁可丢弃 ) return resp.json()几个参数说清楚。from设为auto是因为屏幕取词经常是中英混排,自动检测语言方向最省事,代价是可能偶尔把中文单词送去翻译,增加一次无效请求。timeout=2.0是工程里所有网络请求的统一上限,超过直接放弃翻译,不弹窗不阻塞。requests.post用data传参时,库会自动做表单 URL 编码,不需要手拼查询串,这也是签名和传输分离的好处。
用命令模拟一遍更容易排查问题。拿一个词直接测接口,确认自己的签名没问题再集成代码:
curl "https://api.fanyi.baidu.com/api/trans/vip/translate" \ --data-urlencode "q=dbg" \ --data-urlencode "from=auto" \ --data-urlencode "to=zh" \ --data-urlencode "appid=2024xxxx" \ --data-urlencode "salt=123456" \ --data-urlencode "sign=xxxxxxxx"返回 JSON 里trans_result是译文列表,error_code存在说明出错。调试时先用curl把不同文本长度、不同特殊字符的签名都验一遍,再进代码逻辑,能省很多排查时间。特殊字符是重灾区,比如原文里带+、&、空格,签名算的是原始字符串,而传输走的是 URL 编码,两条路不一致就会出现“签名对但请求报错”的诡异现象。
4.3 响应解析与错误码处理:超时、限流与重试策略
百度翻译的错误码不多,但每个都对应一种常见故障。工程包里把这几个错误码单独做了映射,而不是笼统地弹“翻译失败”。
| 错误码 | 含义 | 工程里的处理 |
|---|---|---|
| 52001 | 请求超时 | 丢弃本次结果,不重试 |
| 52002 | 系统异常 | 丢弃本次结果,不重试 |
| 54000 | 签名错误 | 写入日志,提示检查 appid 和密钥 |
| 54003 | 访问频率受限 | 退避 800ms 后重试一次,仍失败就丢弃 |
| 54005 | 查询文本过长 | 按空格切词,只翻第一个词 |
重试策略要克制。屏幕取词的每一次请求都是用户“看了一眼”产生的,结果晚 1 秒回来就没有意义了。所以工程里的重试次数上限是一次,而且只在54003限流时重试,52001超时不重试。重试用退避而不是立刻重发,避免两个请求同时堆在服务端让限流更严重。
解析响应的逻辑里有个容易翻车的点:百度返回的from字段不一定是auto,而是它检测出来的实际语言,比如en。如果工具要做“反向翻译”或“收藏生词”,这个字段才是你该存的源语言。工程里的解析函数会把from、to、dst一起存进本地缓存,这样下次再遇到同一个词时不需要重新检测语言方向。
4.4 本地缓存:同一单词只请求一次翻译
屏幕取词和文档翻译最大的不同是重复率极高。读一页论文,可能incremental、heuristic这种词会被停三遍。如果每停一次都重新请求,既浪费额度又卡操作。工程里的缓存层就是一个简单的 LRU 字典,按词条原文做 key,最大缓存 5000 条。
from functools import lru_cache @lru_cache(maxsize=5000) def cached_translate(q: str) -> str: if len(q) > 64: return "" # 长文本不进缓存,避免内存里堆积段落 result = translate(q) return result["dst"]maxsize=5000是综合考虑内存和词频后定的。取词翻译的句子一般不超过 20 个字符,5000 条单词加短句的内存占用在 10MB 以内,可以接受。len(q) > 64不做缓存,是因为整段复制翻译在取词场景里本来就少见,而且缓存长文本会显著增加内存压力。缓存命中后直接返回译文,弹窗能做到瞬时显示,这是后面调低延迟最依赖的一个模块。
5. 避坑排查:Hook 失效、高 DPI 漂移与翻译延迟的五个现场
这套工具的坑主要集中在系统 API 的边界行为上。下面五条是我在复现和调试过程中真实遇到的现场,每条都按“现象 → 原因 → 解决”整理,遇到类似问题可以直接对号入座。
5.1 Hook 注册成功却没回应:消息循环与杀软拦截
现象:SetWindowsHookEx返回的句柄不为零,说明注册成功,但鼠标移动时回调一次都不进,翻译窗口完全没反应。
原因:低级钩子回调必须由安装线程的消息循环驱动。如果钩子装在 UI 线程,而 UI 线程没有进入Application.Run(),或者线程里做了耗时阻塞操作,消息循环转不动,回调就不会被派发。另一个隐藏原因是安全软件,360 和 Windows Defender 有时会把全局钩子当成键盘记录行为拦截,注册成功但回调被系统过滤。
解决:先确定回调线程里是否有GetMessage()或Application.Run()循环;然后写一个测试版本,在回调里只写日志不干别的,确认回调能进才能继续。杀软拦截的话,需要在杀软里把工具目录加入信任区,或者改用SetWinEventHook监听窗口事件代替鼠标钩子。这条排查路径我已经走了三遍,每次都是先查消息循环,再查杀软。
5.2 取词漂移:UIA 的 Name 把整段落都带回来了
现象:鼠标指到一段英文里的某个词,弹窗里显示的却是一整句话甚至整个段落的译文。
原因:AutomationElement.FromPoint返回的不一定是最精细的文本元素。很多控件把整个 TextBlock 的文本都放在Name里,鼠标落在任何一个字符上拿到的都是完整文本。如果直接把Name送去翻译,结果自然是一段话而不是一个词。
解决:对 UIA 取到的文本先做“就近切词”。工程里的做法是拿到完整文本后,用鼠标坐标算一个相对位置,再按空格把文本切成词数组,根据坐标比例选择鼠标最近的那个词。这个做法的前提是文本是单行的;多行文本的建议是直接放弃 UIA,走 OCR 或剪贴板。切词逻辑不对时还有一个明显症状:鼠标停在行首词时取到行尾词,优先看坐标换算比例是不是写反了。
5.3 高 DPI 与多显示器:坐标换算在不同缩放比例下翻车
现象:在 150% 缩放的笔记本上一切正常,插上 100% 缩放的外接显示器后,鼠标指向 A 窗口却从 B 窗口取词,或者干脆取到空白。
原因:物理像素和逻辑像素混用。WH_MOUSE_LL返回的坐标是物理像素,UIA 的FromPoint接收的是逻辑像素。系统缩放比例不为 100% 时两者不同,跨屏时每个显示器的缩放比例还可能不同。
解决:进程启动时显式声明 DPI 感知级别:
// 必须在创建任何窗口之前调用 SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);声明之后,系统会把所有 API 的坐标统一到物理像素,AutomationElement.FromPoint和钩子坐标就一致了。多显示器场景下还需要记录每个显示器的边界和缩放因子,坐标落点先判断在哪个显示器,再按该显示器的缩放做换算。这条是整套工具里最容易在发布后才暴露的问题,因为开发机往往只有一个缩放比例。
5.4 全屏应用里弹窗消失:独占模式与覆盖层问题
现象:打开全屏游戏或者全屏播放器时,取词弹窗完全不显示,但日志显示翻译请求是正常的。
原因:全屏独占模式下,系统层 GDI 窗口无法覆盖在 DirectX 交换链之上;另外部分应用自己绘制了鼠标交互层,钩子能拿到坐标,但窗口被覆盖层挡在下面。
解决:这属于边界需求,我不建议硬扛。工程里的处理是检测到前台窗口是全屏独占时,取词结果改为写入系统托盘气泡和剪贴板,而不是弹窗。如果是视频播放器的全屏模式,可以提示用户改用窗口模式再看。游戏取词需要走 DLL 注入配合内嵌渲染,那是另一个量级的工程,不在这个工具范围内。
5.5 请求堆积导致鼠标卡顿:回调线程里绝不能放网络操作
现象:快速移动鼠标后,系统鼠标响应变慢,CPU 占用升高,过一会又恢复正常。
原因:翻译请求没有做线程隔离。如果笔误把翻译调用写进了钩子回调,或者后台线程没有做限流,鼠标每停一次就发一个请求,请求没超时就不丢弃,最终在连接池里堆积大量请求。
解决:用单线程工作队列代替每事件一请求。鼠标静止触发取词后,把任务放进队列,队列一次只处理一个请求;新任务进来时先取消队列里未开始的旧任务。同时给每个请求带上版本号,响应回来时对比版本号,过期响应直接丢弃。这套组合拳之后,快速滑过一段英文时最多只会发两个翻译请求,不会排队轰炸。
6. 把取词延迟从 500ms 压到 90ms:四个调优动作
感知延迟不完全等于网络延迟。用户从“鼠标停住”到“看到译文”的经历时间由四段组成:静止判定时间、取词时间、翻译时间、渲染时间。默认配置下每段的经验值是 300ms + 100ms + 200ms + 50ms,加起来超过 600ms,体感拖沓。下面几个动作能把整体压到 90ms 左右。
第一个动作是压缩静止判定。原来的 300ms 去抖是为了防误触,但配合 4px 移动阈值后,可以把静止判定降到 150ms,移动阈值放宽到 6px。误触率几乎不变,但体感明显变快。第二个动作是缓存预热:程序启动时把内置高频词表的前 500 个词异步送进翻译缓存,鼠标第一次停到常用词时直接命中,0 请求 0 等待。第三个动作是弹窗复用:不要每次翻译都 new 一个窗口,窗口创建和销毁的代价比改文本大得多。常驻一个隐藏弹窗,翻译结果回来后更新 TextBlock 和位置再显示,显示隐藏只切换 Visibility,渲染消耗从 50ms 降到 5ms。
第四个动作是响应过期丢弃:
private int _requestVersion; private async Task RequestTranslate(string word) { int version = ++_requestVersion; string result = await _translator.TranslateAsync(word); // 如果取词过程中鼠标已经移走,这个结果直接作废 if (version != _requestVersion) return; ShowResult(word, result); }版本号机制保证屏幕上永远只有最新一次取词的结果,旧请求回来也不会覆盖新内容。四个动作合起来,工程里实测从鼠标静止到弹窗可见的时间稳定在 90ms 左右,其中网络往返占了 70ms,感知上基本是“停住即出译文”。
从那以后,我每次改完坐标换算或延迟参数,都会强制自己在 150% 缩放的副屏上、开着杀软的环境里、快速滑过一段英文各跑一遍,再确信发布了。希望帮到你。
本文还有配套的精品资源,点击获取