Win32 IME 編程心得【轉】

一些術語

  • IME: Input Method Editor/Engine, 輸入法編輯器, 引擎
  • IMM: Input Method Manager, 輸入法管理器
  • Comp: Composition String, 一般是用戶輸入的字串, 比如拼音輸入香'字要打xiang', 這個`xiang'就是用戶輸入的Comp. 你可以告訴IMM當前的Comp是什麼, 這樣如果應用能自己顯示Comp/Cands的話它就自己顯示; 你也可以不告訴IMM, 這樣你自己負責顯示就行了.
  • Cands: Candidates, 候選詞組
  • Commit: 提交上屏

其他一些軟鍵盤啊, GuideLine啊之類的對我的輸入法沒什麼用, 我就全部無視了.

簡介

Win32下的輸入法編程概括地來說就是要寫一個DLL. 這個DLL要實現並在.def文件中指定輸出M$指定的一些API. Win32在裝載你的輸入法DLL時會檢查是不是每個API都能查詢到, 如果不是的話, 這個輸入法就不會被成功的裝載.

建議下載2600.1106版本的win32 DDK, 裏面有區位輸入法的源程序, 我的影舞筆就是參考這個程序寫的. 可以看看裏面的wingb.def文件, 總共輸出了將近20個API. 其實大部分都沒什麼用, 直接套就行了, 重要的也就5~6個.

$cat wingb.def
LIBRARY         WINGB

EXPORTS
                ImeConversionList
                ImeConfigure
                ImeDestroy
                ImeEscape
                ImeInquire
                ImeProcessKey
                ImeSelect
                ImeSetActiveContext
                ImeSetCompositionString
                ImeToAsciiEx
                NotifyIME

                ImeRegisterWord
                ImeUnregisterWord
                ImeGetRegisterWordStyle
                ImeEnumRegisterWord

                UIWndProc
                StatusWndProc
                CompWndProc
                CandWndProc

事實上win32 DLL還有一個隱含的輸出函數, 就是DLL的入口函數, 一般都是名爲DllMain的一個函數, 但是在區位輸入法裏這個函數的名字是ImeDllInit. 你可以在你的makefile (或類似於vc6的.dsp/vc789的.vcproj等文件)裏指定入口函數的名字.

初始化的大概的順序是:

  1. DllMain裏註冊窗口類
  2. ImeInquire裏告訴imm你的ime消息窗口的類名
  3. imm根據這個類名創建你的ime消息窗口
  4. 你的消息窗口的回調函數被調用, 消息是wm_create

至此, 用戶就可以開始使用你的輸入法了, 這之後有意義的順序是大概這樣的:

  1. 用戶按下一個鍵
  2. ImeProcessKey返回true, 表示輸入法想處理這個鍵
  3. ImeToAsciiEx被調用
  4. 此函數創建一些ime消息, 如開始/結束輸入法編輯, 設置comp串, cand串, 顯示comp窗口, 顯示狀態欄窗口, 提交等
  5. 相應的窗口被創建, 顯示,
  6. 如果當前的應用程序是懂輸入法的, 它自己也會顯示comp串, 前提是你的ImeToAsciiEx需要告訴它comp串是什麼, 如果你只想自己來顯示comp串的話, 那麼這種程序是不會顯示的

我爲了自己編程方便, 就沒有通知應用程序自己去顯示comp和cands. 同時也是因爲我覺得實在是沒有必要.

DllMain

這個函數肯定是最重要的, 一個DLL沒有這個入口函數的話就不是DLL了. 在DDK的區位輸入法代碼裏有一個sources的文件, 裏面有一行:

DLLENTRY=ImeDllInit

你如果用別的build系統, 比如Visual Studio或者mingw, 你就應該自己配置你的入口函數是哪個.

這個函數在dll load的時候需要初始化你的輸入法裏要用到的全局變量, 以及註冊win32的幾個窗口類. 在區位輸入法裏註冊了四個窗口類, UI, Status, Comp, Cand. 其中UI窗口是一個純消息窗口, 也是win32 IME必須要求的一個窗口. 這個窗口的類名會在ImeInquire裏傳給win32 IME以便讓win32 IME知道它應該去跟誰通訊. 我的影舞筆輸入法把Comp和Cand的窗口合併爲一個了.

如果這個函數返回false的話那這個DLL就會load失敗, 當前嘗試load你的輸入法的這個程序就沒法用你的輸入法了. 所以在這個函數裏你可以幹一些很``酷'' 的事情, 比如, 在測試階段, 你可以指定只有notepad才能成功load你的輸入法, 通過GetModuleFileName你可以得到當前調用的程序的路徑, 如果不是notepad, 那就不讓它用你的輸入法. 然後呢, 你在win32的控制面板→區域設置裏指定你當前正在測試開發的輸入法爲默認的輸入法, 這樣你一打開notepad, 就可以開始測試你的輸入法了, 而不需要按一下輸入法切換鍵才能開始測. 雖然只是省下按一個鍵, 但是也是值得的, 因爲相信你會按很多次的. 而這時候其他的程序不會受影響, 你可以隨便殺死notepad.exe, 做下一輪的開發, 測試迭代.

(win32下一個DLL被load了的話, 是不允許替換這個dll文件的. 所以當你發現一個bug, 做了修正, 你沒法把build出來的這個新的dll拷到系統路徑裏, 必須先把所有的load了這個dll的程序殺死. 如果你的輸入法不是默認的, 那你每次都要按一下切換鍵才能開始測試; 如果它是默認的但是你不把除了notepad的其他程序排除的話, 你每次都要殺死很多程序, 比如explorer.exe等. 尤其是如果你只能手工一個一個的刪的話, 很快你會瘋掉的. 你甚至都不知道哪個程序load了你的輸入法. 只能一個一個的猜? 如果你知道sysinternal的process explorer的話, 那你還可以用一下它的查找功能).

還有一個特別有用的功能是, 即使你的輸入法還有bug, 但是如果這個bug只是針對某個程序的話(或者說某個程序有bug, 但只針對你的輸入法:-), 你可以把這個程序排除在外. 比如, Cygwin下的X窗口程序都是由xwin.exe來畫窗口的, 這些窗口都不能處理win32的輸入法, 但是win32的輸入法切換鍵又能把輸入法的狀態欄給切出來, 很明顯沒什麼意義, 我就把xwin.exe在我的輸入法裏排除了. 又比如, 所有的DOS窗口的輸入法處理都是由一個叫conime.exe的程序處理的, 這個程序好像會對我的輸入法提很非分的要求, 我乾脆就把它也拒之門外:-) 以後我就打定主意在終端窗口裏再也不用輸入法了, 呵呵! (造成這個的原因是前面提到的conime是個`懂''輸入法的應用, 它太`懂''了, 它要求你必須設置comp串/cands告訴它知道, 你還不能自己顯示! 我懷疑這個應用的imm是不是壓根就不會幫你創建那個ime消息窗口. 一句話, 它太霸道了).

做這樣的選擇, 我的生活會更簡單.

ImeInquire

這個函數是除了DllMain後第一個會被win32 IMM調用的函數. IMM通過調用這個函數知道你的輸入法有什麼特性. 比如, 除了按鍵消息外, 你是不是還想處理鍵放開的消息. 以下是我的影舞筆的此函數代碼(註釋版)

BOOL WINAPI
ImeInquire(LPIMEINFO lpImeInfo, LPTSTR lpszWndCls, DWORD dwSystemInfoFlags)
{
        if (!lpImeInfo) { //簡單出錯處理
                return FALSE;
        }

        lpImeInfo->dwPrivateDataSize = 0; //IMM會根據這個值自動爲你的輸入法分配一塊內存, 你可以用它來保存一些你的context數據. 我嫌這玩意兒太"聰明"了, 不用之.

        lpImeInfo->fdwProperty = IME_PROP_KBD_CHAR_FIRST | IME_PROP_UNICODE | IME_PROP_IGNORE_UPKEYS | IME_PROP_SPECIAL_UI;

        // IME_PROP_KBD_CHAR_FIRST 是說IMM調用你的ImeProcessKey和ImeToAsciiEx函數之前是不是把按鍵消息的char值算出來, 在第一個整型參數的前兩個字節裏傳給你, 其實你也可以自己算的
        // 如果你不設這個標誌的話那第一個參數就只有低位的兩個字節有意義 (好像是所按的鍵的虛擬鍵值).

        // IME_PROP_UNICODE, 意義應該很明顯了, 一般肯定得設上

        // IME_PROP_IGNORE_UPKEYS, 就是上文說的要不要處理鍵放開的消息. 不設這個標誌就是要處理, 設了就是不處理(IMM針對鍵放開的消息就不會來調你的ImeProcessKey和ImeToAsciiEx了).

        // IME_PROP_SPECIAL_UI, 不記得什麼意思了, 可以上google查一下.

        lpImeInfo->fdwConversionCaps =
                IME_CMODE_NATIVE | IME_CMODE_NOCONVERSION;
        lpImeInfo->fdwSentenceCaps = 0;
        lpImeInfo->fdwUICaps = UI_CAP_ROT90;
        lpImeInfo->fdwSCSCaps = SCS_CAP_COMPSTR | SCS_CAP_MAKEREAD;
        lpImeInfo->fdwSelectCaps = (DWORD) 0;

        // 這之上的這段代碼是什麼意思我也不明白, 但是這樣寫對我的影舞筆就夠用了, 所以我也懶得去弄明白.
        
        lstrcpy(lpszWndCls, get_ui_class_name().c_str());

        // 這裏你要把你的UI窗口的類名拷到IMM傳給你的輸出參數裏.
        
        return (TRUE);

        // 一定要返回true, 沒試過這裏返回false會怎樣. 
}

LPIMEINFO的定義可以在ddk的immdev.h裏查到.

ImeProcessKey

win32 IMM在收到一個鍵盤消息之後, 會先問一下這個函數, 你的IME是不是想處理這個鍵, 如果你在這個函數裏返回true, 意思就是你想處理, 那麼imm就會接着調下一個函數ImeToAsciiEx, 否則它就會自己處理這個鍵盤消息.

ImeToAsciiEx

這個函數的返回值有點意思. 返回值應該是你這個函數的這次調用一共給win32產生的多少個消息. 比如用戶輸入了一個完整的五筆編碼, 希望提交他/她選中的候選詞了, 你就把要提交的數據(一個unicode字符串)寫到輸入法上下文的提交字串的內存句柄中, 再把 (WM_IME_COMPOSITION, 0, GCS_COMP|GCS_RESULT|GCS_RESULTREAD) 這樣一個消息添加到輸入法上下文的消息內存中, 把要返回多少個消息加一.

以下是輸入法上下文結構的定義:

typedef struct tagINPUTCONTEXT {
    HWND                hWnd;
    BOOL                fOpen;
    POINT               ptStatusWndPos;
    POINT               ptSoftKbdPos;
    DWORD               fdwConversion;
    DWORD               fdwSentence;
    union   {
        LOGFONTA        A;
        LOGFONTW        W;
    } lfFont;
    COMPOSITIONFORM     cfCompForm;
    CANDIDATEFORM       cfCandForm[4];
    HIMCC               hCompStr;
    HIMCC               hCandInfo;
    HIMCC               hGuideLine;
    HIMCC               hPrivate;
    DWORD               dwNumMsgBuf;
    HIMCC               hMsgBuf;
    DWORD               fdwInit;
    DWORD               dwReserve[3];
} INPUTCONTEXT

UIWndProc

這個函數裏要處理IME消息. 其實UI窗口根本沒有UI, 沒有圖形! 這個窗口是一個純消息窗口, 你不會收到wm_paint的消息. 所以我直接把區位輸入法裏處理wm_paint的回調函數刪了. 我覺得把這個窗口命名爲ImePureMsgWnd更合適一些.

話說回來, 這個IME第一重要的窗口是誰創建的呢? 答案是IMM. 你在區位輸入法的代碼裏不會看到這個窗口被CreateWindowEx. 在DllMain裏你會註冊這個窗口的類, 在ImeInquire裏你會把這個窗口的類名傳給IMM. 之後你就會收到這個窗口的創建消息了. 說明肯定是IMM負責創建了這個窗口.

這個函數根據收到的消息要負責創建Comp, Status等窗口, 移動這些窗口的位置, 等等.

CompWndProc 和 StatusWndProc

這兩個窗口函數最重要的當然是負責畫窗口了.

其他不怎麼重要的API

  • ImeConfigure 這個函數就是按理你應該彈一個對話框給用戶配置你的輸入法的地方了, 像我們這種高級電腦用戶, 對這種功能直接無視.
  • ImeConversionList 這個函數是讓另一個輸入法來反查一個漢字在你的這個輸入法裏是怎麼打出來的. 比如區位輸入法可以指定用微軟拼音來反查一個漢字的拼音. 在區位輸入法的unicode模式下按`9999'';輸入香'', 輸入完後還會繼續顯示編輯窗, 內容是`xiang''. 這麼無厘頭的功能, 無視!
  • ImeDestroy 沒什麼好說的,
  • ImeEscape 也沒什麼好說的, 沒什麼鳥用. 以下是我的代碼:
  • ImeSelect Google出來的文檔是說在這個函數裏初始化或析構你的私有數據, 好吧, 我前面已經說了, 我沒什麼私有數據, 所以這個函數也可以簡化了.
  • ImeSetActiveContext
  • ImeRegisterWord
  • ImeUnregisterWord
  • ImeGetRegisterWordStyle
  • ImeEnumRegisterWord
  • ImeSetCompositionString
  • NotifyIME

可以上這兒去看文檔. 這些函數都沒有什麼用.

一些注意事項

win32輸入法編程的陷阱還是挺多的, 搞不好會很迷惑.

  1. 一定要用版本管理工具(廢話), 但是用了版本管理工具還不一定夠, 在前期開發的時候, 要時不時地重啓一下機器, 有時候測着沒問題, 重啓一次就不行了. 如果改動量大的話就會不知道是哪個版本引進的問題.
  2. ime消息窗口的類名不能隨便換. win32的IMM好像裝載過一次以後就會把你這個輸入法的各個窗口的類名給記下來, 你要是換了的話下次就裝載不上了, 必須重啓一次機器. 靠, 發現這個問題當時花了我很多時間.
  3. 某種情況下winlogon.exe不能被排除在外. 如果你的輸入法開發的差不多了, 你把它設成默認的輸入法, 準備以後一直用它了, 嗯, 用的好好的, 一重啓, 嘿, 用不了了. 這是因爲winlogon是你的第一個用戶, 而這個進程好像比較特別, 如果它load這個默認輸入法失敗的話, windows就會認爲這個輸入法有問題. 所以你想把winlogon排除在外的話記得重啓前要把另一個輸入法換成默認的再重啓.
  4. 出現以上兩個問題時你也可以不重啓, 只要把你的.dll換一個名字, 在註冊表裏換一個註冊鍵.

往註冊表裏添的時候內容大概是這樣的: (E0330804的0804比較重要, 這代表這是中文輸入法, 更關鍵的是你的資源文件.rc裏面也有0804, 如果這個不匹配的話這個ime也是load不了的. 前面的e033可以隨便換).

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\E0330804]
"Ime File"="ywb.dll"
"Layout File"="kbdus.dll"
"Layout Text"="Chinese (Simplified) - YWB"

影舞筆的運行方法

編譯用VS2008, 同時還要求安裝了python3.1, 安裝路徑必須是C:/python31, 同時你必須把代碼co到Q:\gcode\scim-cs下, 如果沒有Q:盤可以用subst.exe掛一個. 當然也可以自己改一下源代碼.



原文:http://code.google.com/p/windows-config/wiki/Win32IME


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章