一直想寫一點關於輸入法編程的東西,今天終於有點時間,希望對後來者有點幫助。在此要特別感謝“自由拼音”的作者李振春,我剛開始的幾個問題都是在他的幫助下才解決。
首先我們需要明白輸入法是什麼東西。目前常用的輸入法基本上有兩種類型:外掛式(如早期的萬能五筆)及輸入法接口式(Input Method Editor-IME)。外掛式比較簡單,就是一個exe文件,通過模擬一些Windows輸入消息來給當前處於活動狀態的編輯窗口輸入文字,一個顯著的優點是輸入法只要啟動一次,就可以在所有進程中使用;但缺點不不容忽視,首先實現起來也不容易,一個更大的不足是兼容性不夠好,通常一個Windows版本需要一人對應的輸入法版本,此外這類輸入法為了能夠截獲用戶輸入,通常需要掛接鍵盤鉤子,容易造成系統不穩定或者效率不高。大部分的輸入法還是采用IME來實現,下面本文主要討論一下IME編程需要注意的問題及解決辦法。
IME是什麼?IME是在Windows平台上使用的標准的輸入法接口規范。它實質是一個DLL,Windows為這個DLL定義一系列的接口,不同的接口實現指定的功能。程序員在編寫輸入法程序時只需要實現這些接口並導出就可以作為輸入法使用。關於具體接口的定義不是本文的重點,如果您需要了解只需要在網絡中搜索“輸入法編程指南”就可以明白 ,更多信息參考MSDN。
剛開始輸入法編程最棘手的問題通常是程序框架搭好了卻不知道如何使用及調試。這裡涉及到一個很重要的問題就是輸入法的安裝。輸入法就是Windows的一個插件,需要先進行注冊,Windows才能識別並使用。為此您需要先將您生成的DLL復制到系統目錄(Windows\System32)再調用API ImmInstallIME就可以實現了,在我的實踐中是先編一個簡單的程序來做安裝工作,在每次輸入法重新編譯完成以後調用一次以完成輸入法的注冊。這裡還有一個需要注意的問題是:Windows提供了一種機制,它允許輸入法程序一旦啟動就就不再退出,這就意味著如果你的程序代碼經過修改需要重新安裝時將不得不重新啟動電腦。在IME定義的接口中有一個接口是提供IME的初始化的,它就是BOOL WINAPI ImeInquire(LPIMEINFO lpIMEInfo,LPTSTR lpszUIClass,LPCTSTR lpszOption),下面的代碼來自我寫的輸入法:
BOOL WINAPI ImeInquire(LPIMEINFO
lpIMEInfo,LPTSTR lpszUIClass,LPCTSTR lpszOption)
{
lpIMEInfo->dwPrivateDataSize = sizeof(CONTEXTPRIV);//系統根據它為INPUTCONTEXT.hPrivate分配空間
lpIMEInfo->fdwProperty = IME_PROP_KBD_CHAR_FIRST |
#ifdef _UNICODE
IME_PROP_UNICODE |
#endif
IME_PROP_SPECIAL_UI |
IME_PROP_END_UNLOAD ;
lpIMEInfo->fdwConversionCaps = IME_CMODE_FULLSHAPE |
IME_CMODE_NATIVE;
lpIMEInfo->fdwSentenceCaps = IME_SMODE_NONE;
lpIMEInfo->fdwUICaps = UI_CAP_2700;
lpIMEInfo->fdwSCSCaps = 0;
lpIMEInfo->fdwSelectCaps = SELECT_CAP_CONVERSION;
_tcscpy(lpszUIClass,CLSNAME_UI);
return TRUE;
}
lpIMEInfo->fdwProperty告訴Windows系統您編寫的輸入法的一些特征,注意一下IME_PROP_END_UNLOAD這個標志,有了它您編寫的輸入法會隨著啟動您的輸入法的應用程序(如NotePad)的退出而退出,否則它將長駐於系統中,這也是為什麼很多輸入法在升級安裝時需要首先重新啟動電腦的原因。
在這個接口中還有一點需要特別注意,那就是lpIMEInfo->dwPrivateDataSize,至少我是經過很多次測試才基本證實Windows根據該值為INPUTCONTEXT.hPrivate分配空間。此外如果您修改了這個接口,按照我個人的經驗是需要重新調用ImmInstallIME來安裝。
在安裝完成後,在輸入法列表中應該已經有了您自己的輸入法。點擊調試,由於它是一個DLL,您需要先選擇一個宿主程序,一般選擇“記事本”,以調試方式啟動“記事本”後,在這個“記事本”中打開您的輸入法,您就可以在源代碼中設置斷點了。需要說明的是,VC6.0調試DLL不太好用,首先需要打上SP5或者SP6,這樣也不能夠在DLL啟動的時候就設置斷點,推薦使用.net來調試。
輸入法上下文(HIMC):HIMC是什麼?在輸入法編程時必然要接觸到輸入法上下文這個術語,剛接觸時聽起來實在是半懂不懂。由於輸入法是一個插件,它需要和調用它的應用程序通訊,在輸入法中生成的編碼及重碼信息保存在哪裡應用程序才能正確的讀取呢?答案就在於輸入法上下文。輸入法上下文是由User.exe(一個系統進程)為應用程序分配的內存句柄,在應用程序中啟動的輸入法在這塊內存中寫入數據,User.exe再將數據傳遞到應用程序。
UIWnd:在IME中需要導出一個接口,原型如LRESULT WINAPI UIWndProc(HWND hUIWnd, UINT message,WPARAM wParam, LPARAM lParam),hUIWnd是由User.exe傳過來的窗口句柄,它是輸入法中創建的窗口如編碼窗口,重碼窗口,狀態欄窗口的宿主(Owner),初學輸入法編程的人可能會問這個窗口顯示在哪裡呢?其實它並不是一個普通的窗口,它只是一個用來傳遞Windows消息的窗口(Message Only),在使用時,您不需要關心它在哪裡,只需要使用它就好了。
一個IME需要導出19個(Win98版本)接口,但是對於一個只需要實現一般意義的文字輸入的軟件,您只需要實現幾個基本的接口就可以讓輸入法正常工作了。下面逐一介紹一下這幾個接口。
/**********************************************************************/
在這個接口中,系統通知輸入法當前是否打開了輸入法輸入。一般輸入法啟動時會調用一次,在一些軟件(如EmEditor)中提供打開與關閉輸入法的功能就是通過這個接口實現的。如果打開輸入法,一般會在這個接口中做一些數據的初始化工作。
/* ImeSelect() */
/* Return Value: */
/* TRUE - successful, FALSE - failure */
/**********************************************************************/
BOOL WINAPI ImeSelect(HIMC hIMC,BOOL fSelect)/***********************************************************************/
/*系統調用這個接口來判斷IME是否處理當前鍵盤輸入 */
/*HIMC hIMC:輸入上下文 */
/*UINT uKey:鍵值 */
/*LPARAM lKeyData: unknown */
/*CONST LPBYTE lpbKeyState:鍵盤狀態,包含256鍵的狀態 */
/*return : TRUE-IME處理,FALSE-系統處理 */
/*系統則調用ImeToAsciiEx,否則直接將鍵盤消息發到應用程序 */
/**********************************************************************/
BOOL WINAPI ImeProcessKey(HIMC hIMC,UINT uKey,LPARAM lKeyData,CONST LPBYTE lpbKeyState)
觀察注釋您可以看到在個接口是用來判斷用戶敲擊的哪個鍵需要處理,哪個鍵又應該交給系統自己處理,如果輸入法需要自己處理用戶輸入的鍵,則在這個接口中返回true,否則返回false。/****************************************************************************************************************/
這個接口可以說是輸入法最重要的部分,程序員需要在這個接口中實現編碼與重碼的轉換,轉換完成或者顯示在編碼窗口及重碼窗口,或者發送到應用程序。由於在這個接口中沒有傳入窗口句柄,如果通知輸入法程序的窗口更新顯示呢?當然我們可以使用全局變量,在此我個人推薦的方法是使用IME消息(沒有什麼道理),您將消息類型、參數保存到lpdwTransKey指示的緩沖區中,User.exe會根據消息類型做相應的處理並傳遞到UIWnd這個窗口中。
/* function:應用程序調用這個接口來進行輸入上下文的轉換,輸入法程序在這個接口中轉換用戶的輸入 */
/* UINT uVKey:鍵值,如果在ImeInquire接口中為fdwProperty設置了屬性IME_PROP_KBD_CHAR_FIRST,則高字節是輸入鍵值*/
/* UINT uScanCode:按鍵的掃描碼,有時兩個鍵有同樣的鍵值,這時需要使用uScanCode來區分 */
/* CONST LPBYTE lpbKeyState:鍵盤狀態,包含256鍵的狀態 */
/* LPDWORD lpdwTransKey:消息緩沖區,用來保存IME要發給應用程序的消息,第一個雙字是緩沖區可以容納的最大消息條數 */
/* UINT fuState:Active menu flag(come from msdn) */
/* HIMC hIMC:輸入上下文 */
/* return : 返回保存在消息緩沖區lpdwTransKey中的消息個數 */
/****************************************************************************************************************/
UINT WINAPI ImeToAsciiEx (UINT uVKey,UINT uScanCode,CONST LPBYTE lpbKeyState,LPDWORD lpdwTransKey,UINT fuState,HIMC hIMC)
那麼如何輸入文字呢?要輸入文字需要3個消息配合使用,分別是WM_IME_STARTCOMPOSITION、WM_IME_COMPOSITION和WM_IME_ENDCOMPOSITION,它們分別指示開始輸入編碼,輸入編碼或者結果(視參數而異)及編碼輸入完成。在開始編寫輸入法的時候,為了省事,我的輸入法在用戶確定要輸入一個重碼時才連續調用這3個消息以向編碼器中輸入文字。由於WM_IME_STARTCOMPOSITION和WM_IME_ENDCOMPOSITION需要成對使用,這種方法可以確保它們配對。最初這種方式工作得很好,但是後來發現在一些軟件中出現兼容性問題。如“智能五筆”在“遨游”中就存在這個問題,在“遨游”中的地址欄中打開“智能五筆”,當需要使用回退鍵來刪除錯誤輸入的編碼時,會發現刪除的不是編碼窗口中的編碼而是編輯器中的文字。這是因為類似“遨游”這類軟件主動接管了按鍵輸入如處理一些控制鍵,當它發現這些控制鍵不在WM_IME_STARTCOMPOSITION和WM_IME_ENDCOMPOSITION這兩個消息之間時就自己處理控制鍵而不是先交給User.exe了。因此正確的流程應該是在開始輸入編碼時發送WM_IME_STARTCOMPOSITION,輸入結束後發送WM_IME_ENDCOMPOSITION消息。
/**********************************************************************/
/* UIWndProc() */
/* IME UI window procedure */
/**********************************************************************/
LRESULT WINAPI UIWndProc(HWND hUIWnd, UINT message,WPARAM wParam, LPARAM lParam)
這是一個非常重要的接口,基本上一它負責各種消息的傳遞。一般您需要在這個接口中根據不同的消息類型,實現輸入法窗口(如編碼窗口、重碼窗口、狀態欄窗口)的顯示、隱藏及更新等操作。這個接口實現的功能可能非常復雜,視情況而異,在此就不做更加深入的說明了。在使用時可以參見示例工程。BOOL WINAPI ImeConfigure(HKL hKL,HWND hWnd, DWORD dwMode, LPVOID lpData)
這是最後一個需要注意的接口,在顯示輸入法屬性配置時會Windows會調用這個接口。
基本的接口就介紹到這裡,下面談一談我個人在編寫輸入法程序時遇到的一些問題或者發現的一些需要注意的地方。
1、關於輸入法窗口:閱讀一些輸入法的代碼會奇怪,為什麼輸入法窗口在創建時需要指定WM_DISABLE屬性呢?原來是因為如果不指定這個屬性標志,在打開輸入法時,會導致當前的應用程序失去輸入焦點。但是指定了這個標志後,輸入法窗口不能收到鼠標消息怎麼辦?解決的方法就在於WM_SETCURSOR這個消息。這個消息不管窗口是否可用,只要有鼠標在窗口內窗口都會收到。您可以在這個消息中模擬鼠標消息也可以選擇調用SetCapture這個函數,這樣窗口就可以收到鼠標消息了。
2、關於窗口模式:使用了幾種輸入法後,你會發現,有的輸入法的編碼窗口和重碼窗口是一個窗口,有的又是兩個窗口,它們有什麼區別?或者有的人會覺得這個問題很可笑,但是當您研究了一段輸入法可能就會發現您也有類似的問題:因為在輸入法的導出接口中關於用戶界面的函數就有4個,其它3個分類對應3個窗口回調函數。事實上它們並沒有本質的區別,關鍵在於您的輸入法的使用范圍。一些軟件(如某些游戲)為了界面的整體美觀,不希望用戶在打開輸入法時顯示輸入法自己的窗口,而是希望輸入法按照它的意願將輸入法窗口需要顯示的內容顯示在它創建的窗口中,英文稱之為IME Aware。由於我自己的輸入法目標不是在游戲中使用,所在並沒有按照這個規矩來管理輸入法窗口,而是為了簡化,將編碼窗口和重碼窗口顯示的內容放到了一個窗口中。
3、關於自定義消息:UIWndProc在WM_IME_NOTIFY中提供了一個IMN_PRIVATE,最初我理解為這個消息應該和WM_USER一樣,當我需要不只一人自定義消息時只需要在這個ID的基礎上增加值就好了。但事實是您定義的值可能是系統已經占用的(視Windows的版本而異),您能夠使用的自定義消息應該只有這一個,為了指示多個消息類型,我使用的方法是在WM_IME_NOTIFY的LPARAM中進行區分。
4、調試信息輸出:一般編寫輸入法都不會使用MFC,為了輸出調試信息,一般只能使用OutputDebugString這個API,在示例代碼中的helper.c中我編寫了一個模擬TRACE的函數Helper_Trace,您可以用這個函數來將調試信息輸出到調試窗口。
5、最後再談一談輸入法類型:前面提到輸入法分為外掛式和IME兩種,但是目前一些輸入法發展了第3種類型,那就是結合這兩種類型的優點。例如拼音加加,啟動拼音加加您會發現進程列表裡會多一個拼音加加的服務進程,其實它才是拼音加加輸入法的內核即數據處理部分。拼音加加的IME部分只是一個外殼,它提供傳統的IME輸入法一樣的系統兼容性。在我的輸入法中也采用了這種結構,使用內存文件映射及普通的Windows消息結合來實現兩個進程間的通訊。您可以在我的輸入法的源代碼中找到進程間的通訊源代碼及輸入法代碼。
好象沒有更多的經驗可言,總之,輸入法其實並不神秘,在我看來,只要能夠在VC中跟蹤代碼,我就不相信我會搞不定它!
一家之言,如果有什麼錯誤,還請大家批評指正。
關於示例代碼:示例代碼是我編寫的一個最基本的輸入法程序的框架,它顯示您輸入的編碼,並顯示一個固定的重碼,輸入空格後實現該重碼上屏的功能。通常我們能找到的代碼是一個完整的工程,這樣對於初學輸入法編程的人可能會陷入大量的非輸入法編碼框架的閱讀中,對於實際的輸入法編程並沒有多大的意義。這份代碼就是為了讓您擺脫那些無謂代碼的閱讀。
如果需要更加完整的輸入法代碼,推薦參考“自由拼音”的源代碼,當然也可以參考我寫的輸入法《啟程輸入之星》的代碼,您可以在我的網站上找到下載,http://www.setoutsoft.cn。這份代碼的界面部分我自認為是當前的輸入法中寫得非常出色的。
本文配套源碼