天天看點

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

原文: 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 95和Windows 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視窗類方式實作圖形界面,是以沒有上述到處函數,隻有标準的15個IME導出函數。如果你熟悉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.h和imm32.lib,在Win98DDK中可以找到(名字不是這個名字,需要重命名),或者直接從别人的源碼裡面找到。另外imm.h裡面還定義了一些IME的結構體,這些結構體在IME程式設計中必須用到,具體介紹參考《

》。

3       IME調試環境配置及安裝

建構IME檔案結構我們不必從零開始,可以在别人的源碼上進行修改,這裡以啟程的示例源碼為例(《淺談輸入法程式設計》

http://www.setoutsoft.cn/Html/?256.html

源碼

imesample.rar

),該源碼結構清晰簡潔。

3.1    配置步驟

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

第二步:配置IDE。編譯上述源碼,發現170個錯誤,是沒有配置

imm.h和imm32.lib路徑所緻。在項目屬性->VC++目錄->包含目錄下面添加源碼檔案夾下的IMM檔案夾路徑(包含imm.h和imm32.lib)。編譯成功。

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

第三步:

imesample.ime安裝程式制作。建立Win32控制台應用程式命名為IMEInstaller,将imm.h和imm32.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.h和Imm.h頭檔案(順序不能颠倒,因為imm.h需要依賴前一個檔案的定義)和引用imm32.lib,進而使用ImmInstallIME函數注冊輸入法。

第四步:安裝imesample.ime。編譯上一步的安裝程式,得到“

IMEInstaller.exe”可執行程式,将其拷貝到imesample工程編譯結果imesample.dll所在目錄,将imesample.dll重命名為imesample.ime,然後運作IMEInstaller.exe,如果顯示注冊成功的結果則安裝成功,你能在輸入法選擇裡面看到你剛注冊的輸入法了,如下圖中“我的輸入法”。

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

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

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

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

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

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

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

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

确認修改後,就可以調試了。打開源碼内的imm.c檔案,找到ImeToAsciiEx函數,在其内設定一個斷點。按F5,IDE會啟動你設定的調試程式,将輸入法切換到“我的輸入法”,輸入字母會發現IDE到達斷點處,調試成功。

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

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

3.2    配置說明及注意事項

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

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

1)      編譯生成imesample.ime(假設直接生成的就是.ime字尾的檔案)

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

3)      用ImmInstallIME函數注冊輸入法時,必須保證SysWOW64和注冊函數所在目錄都有imesample.ime庫檔案,否則無法注冊成功。可以重複注冊多次,但隻在系統資料庫添加一條記錄。

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

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

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

這條指令在每次編譯之後都會執行一次,将生成的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結尾的是簡體中文輸入法項目,一般新注冊的輸入法都在最後面,如圖,我們注冊的輸入法系統資料庫編号為E02C0804。

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

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

IME輸入法程式設計心得 1       IME簡介 2       IME結構 3       IME調試環境配置及安裝 4       IME程式設計心得

是以,輸入法的解除安裝就是安裝的逆過程:删除上述兩條系統資料庫項目,然後删除System32 (SysWOW64)下的ime庫檔案即可。

輸入法注冊失敗有幾種可能原因,争對啟程輸入法的注冊,如果失敗,第一是System32 (SysWOW64)和安裝程式目前目錄下沒有對應的ime庫檔案,第二是ime庫檔案編譯問題,需要重新編譯一下,第三是注冊過程被防毒軟體攔截(在第一條系統資料庫内會出現空的系統資料庫項)。

如果是自己編寫空殼輸入法,則可能還會出現其他一些問題,如導出庫檔案名與def定義不一緻,資源資訊為設定成輸入法資訊等,詳情見《

Win下的輸入法(IME)程式設計(1) Win下的輸入法(IME)程式設計(2)

4       IME程式設計心得

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

4.1    準備工作

首先應該确認手頭有《

》兩份原始文檔的列印版,因為随時可能去查閱上面的資訊資料,在我寫這篇介紹文的時候,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, tagCANDIDATEINFO和tagCANDIDATELIST四個結構體,他們具體的參數含義可以參考前文所述各文檔。

tagINPUTCONTEXT是IME最重要的内部資料結構,存儲輸入上下文資料,定義如下:

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句柄擷取該結構體。這裡要提到的是hCompStr和hCandInfo兩個變量,他們都是由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句柄中取得。其中dwCompStrLen和dwCompStrOffset分别表示使用者輸入字元串的長度和偏移量(即lpCompStr首位址+ dwCompStrOffset值指向的記憶體位址為使用者輸入字元串存放位置的首位址)。

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

typedef struct tagCANDIDATEINFO {

DWORD  dwCount;

DWORD 

dwOffset[32];

DWORD  dwPrivateOffset;

} CANDIDATEINFO;

該結構體可以通過LPCANDIDATEINFO lpCandInfo = (LPCANDIDATEINFO) ImmLockIMCC(lpIMC->hCandInfo)的方式從HIMCC句柄中取得。其中dwCount和dwOffset[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,大部分任務就是編寫好ImeProcessKey和ImeToAsciiEx兩個函數,如果不是編寫完全符合IME規範的輸入法,隻需要注意幾個重要函數即可。

4.4    感想

IME的架構體系還算清晰,但開發文檔匮乏,沒有能夠詳細介紹IME開發的資料,進行IME開發最大的難處就是對這些結構體的操作了,沒有比較清晰的範例可以拿來作參考。特别對于不熟悉WIN32API和Windows内部消息循環機制的開發人員,涉及到這種用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程式設計基礎才行,特别是在這種資料匮乏的前提下。