做一個「在任何地方打字都能即時翻譯」的工具,最直覺的做法是攔截鍵盤。 我沒有這樣做,因為那會讓中文、日文、韓文的輸入法整組失效。 這篇說明原因,以及我改用什麼做法。
Windows 提供低階鍵盤掛鉤(SetWindowsHookExW(WH_KEYBOARD_LL, ...)),
可以在按鍵送進任何應用程式之前先看一眼,甚至把它吃掉。
對翻譯工具來說這非常誘人:使用者在任何輸入框打字,我都能即時看到, 翻譯完再模擬按鍵補回去。使用者完全感覺不到中間發生了什麼事。
問題出在「吃掉按鍵」這個動作。
注音、拼音這類輸入法的實際流程是:
ㄋ、ㄧ、ˇ關鍵在第 1 步到第 3 步之間完全沒有「已完成的文字」, 只有一串還沒定案的按鍵。
低階鍵盤掛鉤如果回傳 1(吃掉按鍵),這些按鍵根本不會進到輸入法。
輸入法收不到 ㄋ,自然組不出字。
| 你按了什麼 | 期望 | 攔截鍵盤後實際發生 |
|---|---|---|
ㄋ ㄧ ˇ | 出現候選字「你」 | 什麼都沒出現 |
| 選字鍵 | 「你」被送進輸入框 | 沒有東西可以選 |
| 改打英文 | 正常 | 正常(所以一開始不容易發現問題) |
這也是為什麼這類問題常常「上線後才被回報」—— 開發者自己用英文測試時一切正常。
聽起來很簡單:偵測到輸入法正在組字就放行,否則才攔截。實際上有三個障礙:
ImmGetCompositionString 需要目標視窗的 IME context,
但低階掛鉤是在別人的執行緒之外被呼叫的,拿不到。
結果就是做出來的東西「大部分時候可以」, 但中文使用者會不定時遇到打不出字,而且難以重現。
| 攔截鍵盤 | 不攔截(卡片輸入) | |
|---|---|---|
| 操作感受 | 看起來像魔法,直接在原輸入框打字 | 多一張卡片,但按鍵行為完全正常 |
| 中文/日文/韓文 | 組字被打斷 | 正常 |
| 密碼欄位 | 可能一併被攔截 | 不會經過,因為卡片是獨立視窗 |
| 需要處理的邊角 | Backspace、選字、貼上、拖曳選取、組合鍵…每一種都要判斷 | 作業系統與輸入法自己處理 |
左邊那一欄的「邊角」比想像中多。一旦攔截了鍵盤,就等於要重新實作 一輪編輯器的行為——而且永遠追不上真正的編輯器。
Ctrl+Alt+TEnter,譯文回填到原本的輸入框關鍵是沒有人在猜你在打什麼。你打的字是進入一個真正的文字方塊, 作業系統與輸入法用它原本的方式運作。
這樣反而少掉很多問題:
全域熱鍵有兩種做法:低階鍵盤掛鉤,或 RegisterHotKey。
RegisterHotKey 讓系統在偵測到組合鍵時送一個 WM_HOTKEY 給你,
按鍵本身仍然照常送到目前的視窗。這正是我要的——
我只想被通知,不想擋住任何東西。
註冊三個熱鍵就夠了:
| 熱鍵 | 用途 |
|---|---|
Ctrl+Alt+T | 開關翻譯模式 |
Ctrl+Alt+C | 把焦點拉回卡片 |
Ctrl+Alt+Enter | 在任何地方直接回填譯文 |
值得一提的是 Ctrl+Alt+Enter 這種「在別人的視窗裡也要生效」的熱鍵,
只有 RegisterHotKey 做得到——鍵盤掛鉤反而做不到,
因為它拿不到「現在是哪個輸入框有焦點」的可靠資訊。
Windows 限制只有前景程序能搶前景。工具列在背景執行,要把焦點交給卡片或還給目標輸入框時, 常常會被拒絕。
繞過的方式是在呼叫前先模擬一次 Alt 的按下與放開:
inject_key(VK_MENU)
inject_key(VK_MENU, KEYEVENTF_KEYUP)
user32.SetForegroundWindow(target)
這個小動作會讓系統認為「使用者剛剛有輸入」,
因而解除前景鎖定。Alt 本身不會產生任何輸入內容,是安全的選擇。
把譯文送進別人的輸入框有兩種做法:逐鍵模擬,或透過剪貼簿。
逐鍵模擬(SendInput)聽起來比較乾淨,實際上很容易出錯:
中文、日文、emoji 不一定能用 VK_PACKET 正確送出,
而且會觸發目標應用程式的按鍵處理邏輯。
剪貼簿 + Ctrl+V 幾乎在所有應用程式都可靠,
唯一的缺點是會覆蓋使用者原本的剪貼簿內容。
這是一個小但容易卡住的地方。
Tk 的 geometry() 把負數座標解讀成「距離畫面右側/下側多少」,
所以「把視窗移到左邊那顆螢幕」這種需求會直接失敗,視窗會跳到奇怪的角落。
解法是繞過 Tk,直接用 Win32 的 SetWindowPos,
並帶上 SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE——
只改位置,不動大小、不動疊放順序、也不搶焦點。
兩顆螢幕縮放比例不同時,未宣告 DPI 感知的程序會拿到被系統換算過的座標, 視窗就會偏移。開頭先宣告一次即可:
user32.SetProcessDpiAwarenessContext(-4) # PER_MONITOR_AWARE_V2
掛鉤不是邪惡的,只是被用錯地方。如果工具的目的是記錄或阻擋按鍵 (例如密碼管理員、巨集錄製),掛鉤就是正確的工具。
差別在於你的工具需不需要知道「使用者打了什麼字」。 需要,就得面對輸入法的問題;不需要,就不要碰鍵盤。
「不攔截鍵盤」讓我少掉一整類 bug,代價只是多一張卡片。 對一個要在各種應用程式裡工作的工具來說,這個交換很划算。
如果你也在做類似的工具,希望這篇能幫你少走一段路。
LinLingo 是把上述取捨實作出來的 Windows 即時翻譯工具:
按 Ctrl+Alt+T 開卡片、打字、Enter 回填,
注音與拼音都能正常組字。
看說明 ·
下載 ·
原始碼