2008年10月8日 星期三

Ogre中文輸入 Final

最新的精簡修改版本→Ogre中文輸入Final修改
想看長篇心得的可以繼續看

嗯…還是從頭講起好了
Ogre是個免費而又強大的OpenSource 3次元圖像引擎
但對於想開發中文(或所有其它多字元)的程式來說
輸入法的無法使用是個大問題(現在顯示已經沒什麼大問題)

原先,所有Ogre的Windows訊息全部被封裝在OgreWindowEventUtilities裡(1.4.X版)
所以實行中文輸入所必須取得的IME訊息均無法正常取得
目前由免費打工仔在簡體網站所製作修改支援中文輸入的部分主要是從這裡切入
不過必須修改原始碼而動到LGPL授權是一個問題
這邊作者也透露了另一個不修改原始碼的可能,直接自行建立自己的視窗,然後手動建立Ogre的Render視窗為子視窗,就這樣取得母視窗的主控制權,進而建立自己想要的訊息處理。

OK,講起來很容易,問題在於要怎麼寫?
Ogre的中國網站只先簡單地點出這段程式。
//假设之前已经执行完创建窗口以及Ogre::Root对象的过程
//hWnd为窗口句柄,root为Ogre::Root类型实例
Ogre::NameValuePairList params;//构造参数
std::stringstream ss;
ss<<hwnd; //窗口句柄
params["externalWindowHandle"] = ss.str();//把窗口句柄做为字符串形式设置到参数中
root->initialise(false);//Ogre::Root对象初始化参数为false,表示手动创建渲染窗口
//下面创建渲染窗口
Ogre::RenderWindow * window = _root->createRenderWindow("name", //名称
        width,//宽度
        height, //高度
        false, //是否全屏显示
        &params);
後續交給讀者...

喔,這可是個說簡單不簡單,說難也不會很難的問題。
如果已經直接讀完ExampleApplication與ExampleFrameListener範例程式碼的人。
手動建立一個Ogre視窗不會很難,你只要照著那個步驟,一個一個建立就行。
可是完全沒用到Windows的基本型式,這也無法先手動產生一個母視窗。
更何況訊息處理咧?還有IME的處理啊!
對於初學者來說,全部都很陌生領域。

所以這邊就大概講一下怎麼繼承ExampleApplication與ExampleFrameListener來做中文輸入。
以改造核心處理的ExampleFrameListener開始。首先建立一個檔案ExampleCFrameListener.h,
並用檔名做繼承的類別名稱。然後除了繼承的,我們需要再加入Singleton與IME用的表頭檔。
#include "ExampleFrameListener.h"
//為了使用Singleton
#include "OgreSingleton.h"
//使用IME函式庫
#include "imm.h"  

建構式建立的地方請直接參考後面的程式碼。
接著就是我們必須設定兩個空的函數去取得轉換完成與還在轉換中的字串
///這邊是取得轉換完成的字串,直接傳給GUI System的injectChar
virtual void GetIMECompResultString(const Ogre::UTFString& tempString) { } ///這邊是取得轉換中的字串,主要是給像新注音需要另外產生視窗來顯示暫時性文字的 virtual void GetIMECompString(const Ogre::UTFString& tempString) { }

然後在這邊加入視窗訊息處理函式。
static LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

這邊有點麻煩的是此函式必須為static設定,所以在裡面的處理需要取得指標來運算。
我們為了把GetIMECompResultString與GetIMECompString往這裡丟,所以才設定此物件有Singleton功能以方便馬上利用(註1)。如果還有其它的方式,麻煩請告知。

既然都到這裡了,那當然,就是把上面那兩個函數放在WM_IME_COMPOSITON的訊息裡囉。
不過當然還是要加一些IME本身的字串取得與處理。
//自己處理的IME轉換
case WM_IME_COMPOSITION:

HIMC hImc = ImmGetContext(hwnd);
//正式取得轉換的字串
if (lParam & GCS_RESULTSTR)
{
//ImmGetCompositionStringA( hImc, GCS_RESULTSTR, tCharPtr, dwBufLen );
DWORD dwBufLen;
wchar_t* tCharPtr;
dwBufLen = ImmGetCompositionStringW( hImc, GCS_RESULTSTR, 0, 0 );
if(dwBufLen > 0)
{
tCharPtr = new wchar_t[dwBufLen];
memset( tCharPtr, 0, sizeof(wchar_t) * dwBufLen );
ImmGetCompositionStringW( hImc, GCS_RESULTSTR, tCharPtr, dwBufLen );
tempString = tCharPtr;
ExampleCFrameListener::getSingletonPtr()->GetIMECompResultString(tempString);
}
}// if(lParam...
//取得轉換中的字串
else if (lParam & GCS_COMPSTR)
{
//ImmGetCompositionStringA( hImc, GCS_RESULTSTR, tCharPtr, dwBufLen );
DWORD dwBufLen;
wchar_t* tCharPtr;
dwBufLen = ImmGetCompositionStringW( hImc, GCS_COMPSTR, 0, 0 );
if(dwBufLen > 0)
{
tCharPtr = new wchar_t[dwBufLen];
memset( tCharPtr, 0, sizeof(wchar_t) * dwBufLen );
ImmGetCompositionStringW( hImc, GCS_COMPSTR, tCharPtr, dwBufLen);
tempString = tCharPtr;
ExampleCFrameListener::getSingletonPtr()->GetIMECompString(tempString);
}
else
ExampleCFrameListener::getSingletonPtr()->GetIMECompString("");

}// if(lParam...
ImmReleaseContext(hwnd, hImc);
break;

之所以要取得轉換中的字串,其實是為了新注音。
因為那個還沒轉好的字(正式輸出)是不可能就主動顯示在要輸入的地方,而舊注音有子視窗飄著沒問題。當然按上下鍵選字時,新注音的視窗也會跳出,不過還是看不到一整串的字。
所以才加了GetIMECompString來加以處理這部分。
先不管Singleton的額外設定,輸入中文最主要的部分在此。

再來是ExampleApplication的改造。一樣建個ExampleCApplication.h的檔案,然後引入舊的ExampleApplication.h與剛剛的ExampleCFrameListener.h。
單純繼承的部分就不說了,然後重點首先在於變數。
請加入
protected:
HINSTANCE mHInstance; // HInstance of application, for ime
HWND hwnd;//HWND of appliction for the new windows


然後再建立一個configure的函式修改如下:
if(mRoot->showConfigDialog())
{
// If returned true, user clicked OK so initialise
// Here we choose to let the system create a default rendering window by passing 'true'
/// But the chinese need a manual window. Set it false for manual.
mWindow = mRoot->initialise(false);

/// then create a new window for IME
setupWindow();
return true;
}


接著就是手動建立視窗的重頭戲了。
理所當然我們建立一個setupWindow()的新函式在後面。
接著取得目前的 hinstance
mHInstance = GetModuleHandle( NULL );

註冊一個WNDCLASS如下:
WNDCLASS wincl = { 0, ExampleCFrameListener::WindowProcedure, 0, 0, mHInstance,
LoadIcon(0, IDI_APPLICATION), LoadCursor(NULL, IDC_ARROW),
(HBRUSH)GetStockObject(BLACK_BRUSH), 0, "OgreChineseWnd" };

RegisterClass (&wincl);

請注意那個ExampleCFrameListener::WindowProcedure就是加在這裡。

接著我們建立並指定視窗的大小位置與視窗類別等等
取得其HWND
hwnd = CreateWindow("OgreChineseWnd", "", WS_OVERLAPPEDWINDOW,
left, top, width, height, HWND_DESKTOP, 0, mHInstance, 0);

這時我希望有個不同過去的中文化標題
SetWindowTextW( hwnd, L"Ogre中文輸入測試視窗");

再來就是照著最先的部分,讓Ogre設定一個同樣大小的視窗,為子視窗
Ogre::NameValuePairList params;
params["externalWindowHandle"] = StringConverter::toString((int)hwnd);
mWindow = mRoot->createRenderWindow("Ogre Render Window", width, height, false, &params);


最後不要忘了,讓母視窗顯示,才不會什麼都看不到
ShowWindow (hwnd, SW_SHOWNORMAL);


這樣大致上的設定就差不多了。
接下來的部分,首先要繼承上面的新類別(廢話 XD)
然後編譯的參數要加入imm32,這樣IME才有作用。
當然也不能忘了,要讓GUI System或Ogre能顯示中文字才有辦法看得到輸入
這部分就不加贅述了
要補充的是輸入的部分。
以CEGUI為例:
在CEGUI::System::getSingleton().injectChar( arg.text );前請加入這一行
if ( !ImmIsIME(GetKeyboardLayout(0)) )
讓IME啟動時,無法輸入英數,才不會在轉換前多了一堆自己Keyin的亂碼。
加入GetIMECompResultString的函式如下:
void GetIMECompResultString(const Ogre::UTFString& tempString)

Ogre::UTFString temp = tempString;
for (int i = 0; i < temp.size();i++)
{
CEGUI::System::getSingleton().injectChar( temp[i] ); }

injectChar只能處理單一字元,所以才會用迴圈一個一個輸入整個字串。
另一個GUI System "QuickGUI"的部分大同小異,要多記得把Ogre的中文顯示設定好(它沒辦法像CEGUI本身自己支援Unicode轉碼,要靠Ogre自已顯示),並使用setSupportedCodePoints
把要中文的字碼先往裡面塞,不然就算有輸入,它也會因為找不到對應的,而自動取消,會白忙一場。

最後,修改的完整範例在這裡:
ExampleCFrameListener.h
ExampleCApplication.h

一些地方略有差異,最主要就是第二個檔案的setupWindow的函式有參考RenderSystem的原始碼
可以自動生成置中且與一開始設定相同大小的視窗。
我想在Win32的環境下,使用中文輸入應該就這樣子吧。
當然還有一些IME的功能沒有很完整地利用,不過我想這個任務就交給其它有更深需求的人來做吧。我想寫的東西只要能輸入名字就很足夠了。

感謝在我研究IME過程給我幫助的網友,不然現在可能還是尾巴會帶出一堆亂碼來的版本。
另外測試的電腦有灌過Unicode補完計畫,我不清楚是否對IME有所影響。因為我所寫的是專門給寬字元(Wide Char)用的GetIme的函式,有可能會在不支援unicode的輸入法出差錯。
還有我的編譯器是mingw的gcc,VC還沒試著跑過,不過我想問題應該不大。

這東西其實搞起來難度不高,而且看來在1.4.X版出來後,就有辦法實做出來。
到底是搞中文的人少呢?還是即然已經有修改程式碼的辦法,就無所謂呢?
總之,斷斷續續,忙裡偷閒地搞了至少3年Ogre。
能實現一種中文輸入成功真的很高興。在此分享給大家。

註1:其實我還是個半生不熟的學習者,只知道可以拿來用,還不知道為什麼可以用,以及是否有其它延伸的問題。還忘多多海涵。

5 則留言:

微風星辰 提到...

大大非常感謝你的幫忙
我現在可以顯示中文了
我打算把剩下的部分試著完成看看
如果有成果出來我會拿來討論看看^^
from weijen 巴哈

Unknown 提到...
作者已經移除這則留言。
Unknown 提到...

感謝提供如此優良的框架
中文輸入總算是有點眉目了
另外這個方法似乎可以支援所有的輸入法?
剛才試了一下發現日文也可以正常顯示的樣子
utf真是邪惡的東西阿


前一篇手殘發錯,重發一次Orz

Unknown 提到...

你的代碼我下不了,請發到我的郵箱:ycjlhy@163.com。謝謝。

zevoid 提到...

如果再 OGRE 視窗的外面再包一層視窗的目的只是為了要攔截 message 的話, 可以試試直接用 SetWindowLong 這個函數, 將自己的 message handler 函數取代掉原本 OGRE 視窗的. 然後保存 OGRE 原有的的 message handler - 也就是 SetWindowLong 的返回值於本地端. 你自己的 message handler 中只處理 IME 相關訊息, 其餘的再送給你保存下來的那個原來的 message handler 去處理.