IME輸入法編程心得


posted @ 2012-11-30 00:42 from [FreedomShe]

自然語言處理的輸入法作業成品沒有做出來,但不想再在蛋疼的Win32上面耗費時間了,整理文檔,記錄一下心得,新手再來研究也不會迷路太遠。

1. IME簡介
2. IME結構
3. IME調試環境配置及安裝
3.1. 配置步驟
3.2. 配置說明及注意事項
3.3. IME安裝及卸載
4. IME編程心得
4.1. 准備工作
4.2. IME數據結構介紹
4.3. IME接口調用順序
4.4. 感想


 

1       IME簡介

什么是IME (Input Method Editors)?廣義上講,IME是微軟提供的Windows平台的一套輸入法編程規范,依照這套規范(框架),你不需要處理太多輸入法特性相關的操作(光標跟隨,輸入捕獲,字碼轉換后輸出到應用程序等),你只需要使用IME規范里面提供的工具函數(imm32庫),實現規范所指定必須導出的接口即可。實際上你要做的就是寫一個導出函數包含IME規范規定的接口的dll,所以,狹義上講IME就是你寫的這個dll

IME源於Windows 95Windows NT 4.0時代,用於統一Windows系統輸入法編程規范,隨着Windows系統版本更新換代,IME的結構基本沒有改變。作者所能找到的最新IME官方文檔為Windows 98/Windows 2000版,原始文檔位於Windows98DDK內(Win32 Multilingual IME Overview for IME Development》和《Win32 Multilingual IME Application Programming Interface)。

進行Windows輸入法編程必須使用IME方式,否則你必須自己解決前文所述諸如光標跟隨,消息截獲(通常會被殺毒軟件視為病毒行為)等輸入法特性問題。實際上使用IME方式也不必完全遵循IME框架的規定實現完全的IME方式輸入法,開發人員完全可以只把IME當做一個轉換接口——只實現其中幾個重要的接口函數,收發消息,然后講包括輸入法邏輯的實現全部交給自己的庫。所以無論從哪方面講,要實現Windows輸入法編程必須學會使用IME

2       IME結構

如前文所述,狹義IME開發就是實現類似“輸入法名字.ime”這樣一個動態庫(編譯的時候通常將.dll后綴改為.ime后綴)。這個庫需要導出如下15個接口函數:

ImeConversionList

ImeConfigure

ImeDestroy

ImeEscape

ImeInquire

ImeProcessKey

ImeSelect

ImeSetActiveContext

ImeSetCompositionString

ImeToAsciiEx

NotifyIME

ImeRegisterWord

ImeUnregisterWord

ImeGetRegisterWordStyle

ImeEnumRegisterWord

而這些函數中很多都是不重要的,進行編程的時候可以不去具體實現。

注意:如果你查看別人的IME源碼,你會發現他們還導出了UIWndProc, StatusWndProc, CompWndProc, CandWndProc四個函數,這四個函數其實不是IME規定需要導出的接口函數,而是四個窗口過程函數,為回調函數。其中UIWndProc是輸入法主窗口的窗口過程函數,實際上是一個沒有界面的空窗口,StatusWndProc, CompWndProc, CandWndProc分別為狀態窗口,輸入窗口,候選碼窗口的窗口過程函數。源碼的作者們通常在動態庫初始化的時候注冊這四個窗口過程函數所屬的窗口類,由於這些窗口是非模態窗口,與模態窗口不同,它們的消息必須經過主程序的消息循環,所以如果不導出其窗口過程函數,上層IME框架無法將屬於這些窗口的消息發送到這些窗口過程函數處理。例如搜狗輸入法由於不是采用WIN32窗口類方式實現圖形界面,所以沒有上述到處函數,只有標准的15IME導出函數。如果你熟悉WIN32編程和Windows消息循環機制就明白我在說什么了。

要實現這些IME規范規定的接口函數,必須用到IMM (Input Method Manager)庫,位於system32目錄的imm32.dll,這個庫提供了訪問控制IME內部結構的方法,這些方法蓋含imm ui functions, imm support functions, himc and himcc managerment functions, ime hot keys and hot key functions, imm soft keyboard functions五個部分,函數均以Imm開頭。與imm32.dll配套的頭文件和lib庫文件為imm.himm32.lib,在Win98DDK中可以找到(名字不是這個名字,需要重命名),或者直接從別人的源碼里面找到。另外imm.h里面還定義了一些IME的結構體,這些結構體在IME編程中必須用到,具體介紹參考《Win32 Multilingual IME Application Programming Interface》。

3       IME調試環境配置及安裝

構建IME文件結構我們不必從零開始,可以在別人的源碼上進行修改,這里以啟程的示例源碼為例(《淺談輸入法編程》http://www.setoutsoft.cn/Html/?256.html源碼imesample.rar),該源碼結構清晰簡潔。

3.1    配置步驟

第一步:下載源碼解壓。在解壓目錄(假定名為imesample)發現該項目是VC++6.0項目,如果你的IDEVisual Studio就需要進行格式轉換。直接雙擊imesample.dsw轉換后就得到了自己的VS解決方案imesample.sln(我的IDEVS2012)。

第二步:配置IDE。編譯上述源碼,發現170個錯誤,是沒有配置imm.himm32.lib路徑所致。在項目屬性->VC++目錄->包含目錄下面添加源碼文件夾下的IMM文件夾路徑(包含imm.himm32.lib)。編譯成功。image

 

第三步:imesample.ime安裝程序制作。創建Win32控制台應用程序命名為IMEInstaller,將imm.himm32.lib拷貝到該工程目錄下,修改主函數代碼為:

// IMEInstaller.cpp : 定義控制台應用程序的入口點。
//

#include "stdafx.h"
#include <Windows.h>
#include "Imm.h"
#pragma comment(lib,"imm32.lib")
int _tmain(int argc, _TCHAR* argv[])
{
	HKL IME = ImmInstallIME(L"imesample.ime", L"我的輸入法");
	if(IME==0)
	{
		printf("注冊輸入法失敗,請注銷(或重啟)計算機再試驗!\n");
	}
	else
	{
		printf("注冊輸入法成功!\n");
	}
	printf("按任意鍵退出!\n");
	getchar();
	return 0;
}

 

主要是添加Windows.hImm.h頭文件(順序不能顛倒,因為imm.h需要依賴前一個文件的定義)和引用imm32.lib,從而使用ImmInstallIME函數注冊輸入法。

第四步:安裝imesample.ime。編譯上一步的安裝程序,得到“IMEInstaller.exe”可執行程序,將其拷貝到imesample工程編譯結果imesample.dll所在目錄,將imesample.dll重命名為imesample.ime,然后運行IMEInstaller.exe,如果顯示注冊成功的結果則安裝成功,你能在輸入法選擇里面看到你剛注冊的輸入法了,如下圖中“我的輸入法”。image

 

如果注冊失敗,重啟計算機也無法解決,原因有幾種可能,在后面的說明中解釋。

第五步:測試剛才注冊的輸入法。打開記事本,如果輸入法選擇菜單項的圖標無法顯示,則表示此應用中無法加載對應輸入法的庫文件,此輸入法無法使用(可能跟編譯版本或者系統有關,我的是WIN7 64位系統),如下圖。image

 

但我的UltraEdit內可以正常使用新裝的輸入法,如下圖。記錄下一個可以正常使用新輸入法應用程序的路徑,例如我的UltraEdit路徑為:"E:\Program Files (x86)\IDM Computer Solutions\UltraEdit\Uedit32.exe"image

 

第六步:配置調試程序。打開imesample工程,找到項目屬性->調試->命令項,將上一步記下的調試程序路徑填入其中,如下圖。image

 

確認修改后,就可以調試了。打開源碼內的imm.c文件,找到ImeToAsciiEx函數,在其內設置一個斷點。按F5IDE會啟動你設置的調試程序,將輸入法切換到“我的輸入法”,輸入字母會發現IDE到達斷點處,調試成功。image

 

以后修改代碼,直接調試即為最新代碼效果(系統能在你的工程目錄下找到最新版本dll並加載?非也,看后文解釋)。

3.2    配置說明及注意事項

下面來分析一下上述步驟的過程及原理。

本來的流程應該是這樣的:

1)      編譯生成imesample.ime(假設直接生成的就是.ime后綴的文件)

2)      然后將imesample.ime拷貝到C:\Windows\System32\目錄(如果你是64位系統,必須拷貝到SysWOW64目錄下,因為64位系統只從SysWOW64中尋找輸入法庫文件)

3)      ImmInstallIME函數注冊輸入法時,必須保證SysWOW64和注冊函數所在目錄都有imesample.ime庫文件,否則無法注冊成功。可以重復注冊多次,但只在注冊表添加一條記錄。

4)      進行調試的時候,系統使用的是System3264位系統為SysWOW64)下的imesample.ime庫文件,所以每次修改代碼編譯后都必須將新版imesample.ime拷貝到System32 (SysWOW64)下才可執行調試,否則斷點為失效狀態。

而實際上我們並沒有復制imesample.imeSystem32 (SysWOW64)啊,原來是作者在工程中做了一個小動作——加入了后期生成時間命令行。即打開工程,工程屬性->生成事件->后期生成事件->命令行,可以發現多了一條命令”copy .\debug\imesample.dll C:\Windows\system32\imesample.ime”image

 

這條命令在每次編譯之后都會執行一次,將生成的dll重命名為ime復制到系統文件夾中。如果你是64位系統也不必修改其中的system32文件夾為SysWOW64,因為在執行命令時會自動修改為SysWOW64去執行。

當某個應用正在使用輸入法時,會動態加載輸入法的庫文件,其庫文件被鎖定無法修改。所以,如果在編譯時出錯,錯誤指向上述copy命令,則需要關閉使用該輸入法的進程才行。另外,IME有一種使輸入法啟動就不再退出的機制,需要在ImeInquire內設置不采用這種機制,否則無法調試(詳情見啟程作者描述)。

配置步驟第二步的改進:工程屬性->常規->目標文件擴展名,將dll改為ime,這樣每次編譯就直接生成.ime文件了,修改后還需要將上述copy命令行里的imesample.dll改為imesample.ime

3.3    IME安裝及卸載

上一節介紹了使用imm32.dll內的ImmInstallIME函數安裝輸入法的方法,實際上這個函數只是在注冊表內添加了兩條記錄。

第一條記錄位於HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\在展開項中,以0804結尾的是簡體中文輸入法項目,一般新注冊的輸入法都在最后面,如圖,我們注冊的輸入法注冊表編號為E02C0804image

 

另外一條記錄位於HKEY_CURRENT_USER\Keyboard Layout\Preload下,表示當前輸入法選擇菜單中列出的輸入法列表,如下圖。image

 

所以,輸入法的卸載就是安裝的逆過程:刪除上述兩條注冊表項目,然后刪除System32 (SysWOW64)下的ime庫文件即可。

輸入法注冊失敗有幾種可能原因,爭對啟程輸入法的注冊,如果失敗,第一是System32 (SysWOW64)和安裝程序當前目錄下沒有對應的ime庫文件,第二是ime庫文件編譯問題,需要重新編譯一下,第三是注冊過程被殺毒軟件攔截(在第一條注冊表內會出現空的注冊表項)。

如果是自己編寫空殼輸入法,則可能還會出現其他一些問題,如導出庫文件名與def定義不一致,資源信息為設置成輸入法信息等,詳情見《Win下的輸入法(IME)編程(1》和《Win下的輸入法(IME)編程(2》。

4       IME編程心得

本文中提到的很多別人的文檔都記錄了自己進行IME編程的一些心得體會,在這里我就不作額外介紹了,只是做一下整理和補充。

4.1    准備工作

首先應該確認手頭有《Win32 Multilingual IME Overview for IME Development》和《Win32 Multilingual IME Application Programming Interface》兩份原始文檔的打印版,因為隨時可能去查閱上面的信息資料,在我寫這篇介紹文的時候,MSDN上沒有跟多的輸入法編程相關資料(以前有,但是現在無法在MSDN上查閱到了),所以面對資料的匱乏,只能從網絡上依靠關鍵詞尋求一些幫助信息,或者閱讀別人的源碼。

然后,通過上面的資料你應該能了解IME框架的三大部分知識(我理解的三大部分)。第一是ime*15個導出函數中重要的幾個導出函數功能(參考《我的win32 輸入法編程心得》https://code.google.com/p/windows-config/wiki/Win32IME);第二是了解imm*眾多函數中重要幾個的功能,例如ImmLockIMC, ImmUnlockIMC, ImmLockIMCC, ImmUnlockIMCC等(參考《輸入法(IME)實現原理》http://blog.sina.com.cn/s/blog_56a388c20100004u.html);第三是了解IME框架內的主要幾個結構體,IME的所有信息都保存在這些結構體中,只有理解了它的結構才能自如地控制IME存取數據。

4.2    IME數據結構介紹

我要重點介紹的是tagINPUTCONTEXT, tagCOMPOSITIONSTR, tagCANDIDATEINFOtagCANDIDATELIST四個結構體,他們具體的參數含義可以參考前文所述各文檔。

tagINPUTCONTEXTIME最重要的內部數據結構,存儲輸入上下文數據,定義如下:

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;

可以認為它是IME內部數據的根節點,通過它可以讀寫IME內部所有信息,它由HIMC句柄表示,可以通過LPINPUTCONTEXT lpIMC = (LPINPUTCONTEXT)ImmLockIMC(hIMC);的形式通過HIMC句柄獲取該結構體。這里要提到的是hCompStrhCandInfo兩個變量,他們都是由HIMCC句柄表示,hCompStr負責用戶輸入字符串的信息,hCandInfo負責候選碼部分的信息,將在后面介紹。

tagCOMPOSITIONSTR是負責管理用戶輸入字符串信息的結構體,為tagINPUTCONTEXT的子節點,定義如下:

typedef struct tagCOMPOSITIONSTR {

DWORD  dwSize;

DWORD  dwCompReadAttrLen;

DWORD  dwCompReadAttrOffset;

DWORD  dwCompReadClsLen;

DWORD  dwCompReadClsOffset;

DWORD  dwCompReadStrLen;

DWORD  dwCompReadStrOffset;

DWORD  dwCompAttrLen;

DWORD  dwCompAttrOffset;

DWORD  dwCompClsLen;

DWORD  dwCompClsOffset;

DWORD  dwCompStrLen;

DWORD  dwCompStrOffset;

DWORD  dwCursorPos;

DWORD  dwDeltaStart;

DWORD  dwResultReadClsLen;

DWORD  dwResultReadClsOffset;

DWORD  dwResultReadStrLen;

DWORD  dwResultReadStrOffset;

DWORD  dwResultClsLen;

DWORD  dwResultClsOffset;

DWORD  dwResultStrLen;

DWORD  dwResultStrOffset;

DWORD  dwPrivateSize;

DWORD  dwPrivateOffset;

} COMPOSITIONSTR;

該結構體可以通過LPCOMPOSITIONSTRING lpCompStr = (LPCOMPOSITIONSTRING) ImmLockIMCC(lpIMC->hCompStr)的方式從HIMCC句柄中取得。其中dwCompStrLendwCompStrOffset分別表示用戶輸入字符串的長度和偏移量(即lpCompStr首地址+ dwCompStrOffset值指向的內存地址為用戶輸入字符串存放位置的首地址)。

tagCANDIDATEINFO是負責管理候選碼信息的結構體,為tagINPUTCONTEXT的子節點,定義如下:

typedef struct tagCANDIDATEINFO {

DWORD  dwSize;

DWORD  dwCount;

DWORD  dwOffset[32];

DWORD  dwPrivateSize;

DWORD  dwPrivateOffset;

} CANDIDATEINFO;

該結構體可以通過LPCANDIDATEINFO lpCandInfo = (LPCANDIDATEINFO) ImmLockIMCC(lpIMC->hCandInfo)的方式從HIMCC句柄中取得。其中dwCountdwOffset[32]分別表示候選碼表的個數和碼表內存位置的偏移量,即通過lpCandInfo首地址+dwOffset[i]就能得到碼表tagCANDIDATELIST的首地址。

tagCANDIDATELIST用於存儲碼表數據,是上面tagCANDIDATEINFO的子節點,定義如下:

typedef struct tagCANDIDATELIST {

    DWORD  dwSize;  // the size of this data structure.

    DWORD  dwStyle;  // the style of candidate strings.

    DWORD  dwCount;  // the number of the candidate strings.

    DWORD  dwSelection;  // index of a candidate string now selected.

    DWORD  dwPageStart;  // index of the first candidate string show in the candidate window. It maybe varies with page up or page down key.

    DWORD  dwPageSize;  // the preference number of the candidate strings shows in one page.

    DWORD  dwOffset[];  // the start positions of the first candidate strings. Start positions of other (2nd, 3rd, ..) candidate strings are appened after this field. IME can do this by reallocating the hCandInfo memory handle. So IME can access dwOffset[2] (3rd candidate string) or dwOffset[5] (6st candidate string).

// TCHAR  chCandidateStr[];  // the array of the candidate strings.

} CANDIDATELIST;

該結構體存儲了一張候選碼表的字符串信息,dwCount為候選碼個數,dwOffset[i]為第i個候選碼的首地址偏移量,同樣通過偏移量加結構體首地址的方式可以定位到候選碼字符串的首地址。

通過上述結構體,已經可以在ImeToAsciiEx函數內做字碼轉換的操作了(主要是存取用戶輸入字符和修改轉換后的候選碼表),然后在用戶輸入窗口和候選碼窗口分別從上述結構體中取出對應字符串顯示出來。

4.3    IME接口調用順序

測試切換到“我的輸入法”輸入兩個字符,然后切換出“我的輸入法”,發現IME接口函數調用順序大致如下:ImeInquire, ImeSelect, UIWndProc, StatusWndProc, ImeProcessKey, ImeToAsciiEx, CompWndProc, CandWndProc, UIWndProc, NotifyIME, UIWndProc, CompWndProc, CandWndProc, ImeProcessKey, ImeToAsciiEx, NotifyIME, UIWndProc, NotifyIME, ImeProcessKey, ImeProcessKey, NotifyIME, StatusWndProc, UIWndProc, CompWndProc, CandWndProc, StatusWndProc, UIWndProc, ImeSelect

其中ImeInquire是最先被調用的函數,當應用程序第一次切換到“我的輸入法”時會調用該函數進行一些初始化操作,應用程序多次切換輸入法均不會再調用該函數。ImeSelect是當用戶切換入“我的輸入法”或者切換出“我的輸入法”時調用的。ImeProcessKey是當用戶發生一次按鍵時調用,開發人員可以獲取按鍵信息,用於判斷按鍵信息是否需要輸入法來處理,如果需要則返回true,截獲按鍵信息交給ImeToAsciiEx函數處理。NotifyIME是應用程序與輸入法交互的函數,可以不用理會。UIWndProc StatusWndProc CompWndProc CandWndProc四個函數之前已經介紹了,分別是主窗口,狀態窗口,用戶輸入字符串窗口,候選碼窗口四個窗口類的窗口過程函數,如果你使用Win32的窗口類來構建自己的窗口,則顯示數據的任務在這些回調函數內編寫,弱否,則不必理會。所以,編寫IME,大部分任務就是編寫好ImeProcessKeyImeToAsciiEx兩個函數,如果不是編寫完全符合IME規范的輸入法,只需要注意幾個重要函數即可。

4.4    感想

IME的框架體系還算清晰,但開發文檔匱乏,沒有能夠詳細介紹IME開發的資料,進行IME開發最大的難處就是對這些結構體的操作了,沒有比較清晰的范例可以拿來作參考。特別對於不熟悉WIN32APIWindows內部消息循環機制的開發人員,涉及到這種用C語言對結構體內存直接做操作的細節處,要正確進行IME開發是難上加難。

實際上,如果你不是希望編寫完全IME規范的輸入法,有幾種方式可以適度地脫離WIN32API的苦海:

1)      定義自己的結構體。IME開發文檔定義了眾多復雜的結構體,這些結構體無非是為了組織一套IME編碼規范,依照這套規范你可以不用太關注過多的IME內部細節。而實際上,如果你只希望用到其中一部分特性,可以通過自定義結構體的方式,強制將IME內部結構體轉換為自己的方便操作的結構體,加速開發,自由拼音輸入法即是如此。

2)      IME+外掛dll方式。這種方式將IME作為一個與Windows系統溝通的橋梁,而輸入法邏輯完全由外部dll實現,最大的好處就是可以利用現今高效的開發語言和工具進行開發。搜狗拼音輸入法即是如此,它的ime只有15個標准導出函數,因此其窗口過程應該是外包給外部邏輯去處理。但要實現這種方式的前提是對15個導出函數有充分理解,其中某些會要求填充一些數據。雖然沒有親自實現,但個人認為可以將IME導出函數包裝在C++工程內,然后實現與C#庫的通訊,以ImeToAsciiEx作為橋梁,將輸入法的界面邏輯完全交給C#工程去處理。

最后,IME輸入法編程不是一朝一夕能夠掌握,需要具備一定的WIN32編程基礎才行,特別是在這種資料匱乏的前提下。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM