最近又開始研究輸入法的bug了。。。真倒黴。。。這次是爲了解決在微軟拼音和谷歌拼音輸入法開啓的時候,Dynamic input輸入的第一個數字(比如圓的直徑)會丟失,這肯定是來自中國客戶的抱怨,而且貌似是個大客戶,上頭催得緊,得罪不起,咋辦,只能研究了呀!
我們之前的邏輯是這樣的:
1. 在View裏面監視KeyDown事件,假如收到的Char是VK_PROCESSKEY,那麼就認爲這是一個輸入法字符,會觸發輸入法的Composition,處理的方法就是:
a)把事件標記成已處理,這樣就屏蔽了WM_CHAR消息;
b)顯示一個空的輸入框~(對於用戶來說,看到的是一個空的輸入框,外加一個輸入法自己顯示的候選詞窗口)。 對於普通的中文輸入法來說(搜狗,QQ),這個是完美的,但是微軟和谷歌則不一樣,在輸入法開啓後,就算你輸入的是數字!他發送的也是VK_PROCESSKEY消息,而且,不會出現候選詞窗口,那麼用戶看到的就是一個空的輸入框了(因爲WM_CHAR被屏蔽了)。。。
2. 那麼解決方案是什麼呢?。。。最簡單的話,就是判斷,這是不是一個真正的IME Char,或者說這個字符到底會不會觸發IME Composition窗口!假如不會,那麼就繼續發送WM_CHAR消息,於是就有了這麼如下代碼:
bool
CAcDynInput::onExternalKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags, bool& bWantOnChar)
{
// IME
if (nChar == VK_PROCESSKEY) {
bool bImeEnterCompositionMode = true;
// DID#1488000. The origin logic is that if nChar == VK_PROCESSKEY,
// we assume it will turn IME into Composition Mode and show the candidate
// words in IME's composition floating window. So we suppress the
// WM_CHAR message and show an empty Dynamic Input Box and user
// will see the IME compostion window near by. But actually, for Microsoft
// Pinyin IME and Google Pinyin IME, if you type number '1', though the nChar
// is still VK_PROCESSKEY, but it won't enter IME composition mode. As a result,
// the char is lost (bcz we suppress WM_CHAR message) and user only sees an
// empty Dynamic Input Box.
//
// To Fix this issue, we need check whether the charactor the user inputs
// will trun the IME into composition mode.
//
AcApView* pView = curView();
if (pView != NULL) {
bImeEnterCompositionMode = pView->isEnterImeCompositionMode();
if (!bImeEnterCompositionMode) {
// To fix DID#1484305 (crash due to focus switching when IME is in
// composition mode), we eat(cancel) the first IME char and re-send
// it after focus is switched. For now, since we changed the logic
// that we didn't suppress WM_CHAR message in this case, then we should
// not re-send it, or we will get double char. So here we suppress
// re-send the eaten char once.
pView->suppressResendEatenImeChar();
}
}
if (bImeEnterCompositionMode) {
// Suppress WM_CHAR and show empty Dynamic Input Box with IME composition
// window near by.
resetKeyDownHandled();
return onExternalChar(nChar, nRepCnt, nFlags);
}
}
。。。。。。
}
bool CAcDwgView::isEnterImeCompositionMode() const
{
bool bEnterCompositionMode = true;
HWND hwnd = this->GetSafeHwnd();
HIMC hIMC = ImmGetContext(hwnd);
if (hIMC != NULL)
{
if (!ImmGetCompositionString(hIMC, GCS_COMPSTR, NULL, 0))
{
bEnterCompositionMode = false;
// Fix for Baidu IME, Baidu IME said that they are using "async" to
// handle ImmGetCompositionString call, so if we call the function in
// WM_KEYDOWN message, it always return false. So for now, there is no
// easy way to tell whether the Baidu IME is really entering composition
// mode.
if (isCurrentImeBaiduPinyinIme())
bEnterCompositionMode = true;
}
ImmReleaseContext(hwnd, hIMC);
hIMC = NULL;
}
return bEnterCompositionMode;
}
我用的是ImmGetCompositionString()函數來判斷,當前輸入法是不是進入了Composition模式,其實他的返回值應該能反應真實的情況,但是有一個萬惡的輸入法,對於你輸入的第一個字符(就是你輸入的觸發輸入法進入Composition模式的第一個字符),其實他已經進入了組詞模式了,但是他返回給你的是Fasle。爲了解決這個問題,我特地在他們官方論壇提交了bug:http://pcbbs.baidu.com/thread-334055-1-2.html,他們回覆告知,他們就是這麼設計的,使用了異步查詢的方法(我一頭霧水。。。)。既然他們無法修改設計,那麼只能我做特殊處理了。isCurrentImeBaiduPinyinIme()函數就應運而生!代碼如下:
bool CAcDwgView::isCurrentImeBaiduPinyinIme() const
{
// Check if the current OS is Win8
// Note, since GetVersionInfoEx will be deprecated in Win8, we can replace the
// following code snippets with IsWindows8OrGreater() API (in versionhelpers.h)
// once we move to Visual Studio 2013.
bool isWindows8OrGreater = false;
OSVERSIONINFOEX osvi;
ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
if (VERIFY(GetVersionEx((OSVERSIONINFO*)&osvi)))
isWindows8OrGreater = (osvi.dwMajorVersion >= 6 && osvi.dwMinorVersion > 1);
DWORD dwThreadId = GetWindowThreadProcessId(this->GetSafeHwnd(), NULL);
HKL hkl = GetKeyboardLayout(dwThreadId);
// Get IME name
CString szImeName;
if (!isWindows8OrGreater)
{
ImmGetDescription(hkl,szImeName.GetBuffer(MAX_PATH), MAX_PATH);
szImeName.ReleaseBuffer();
}
else
{
// Baidu IME in Win8 uses TSF(Text service framework) which no longer
// supports ImmGetDescription() API
HRESULT hr = S_OK;
CComPtr<ITfInputProcessorProfiles> pProfiles;
LANGID langid;
BSTR bstrImeName = NULL;
hr = CoCreateInstance(CLSID_TF_InputProcessorProfiles, NULL, CLSCTX_INPROC_SERVER, IID_ITfInputProcessorProfiles, (LPVOID*)&pProfiles);
if(!VERIFY(SUCCEEDED(hr)))
return false;
hr = pProfiles->GetCurrentLanguage(&langid);
if(!VERIFY(SUCCEEDED(hr)))
return false;
CLSID textSrvId, profileId;
hr = pProfiles->GetDefaultLanguageProfile(langid, GUID_TFCAT_TIP_KEYBOARD, &textSrvId, &profileId);
if(!VERIFY(SUCCEEDED(hr)))
return false;
hr = pProfiles->GetActiveLanguageProfile(textSrvId, &langid, &profileId);
if(!VERIFY(SUCCEEDED(hr)))
return false;
hr = pProfiles->GetLanguageProfileDescription(textSrvId, langid, profileId, &bstrImeName);
if(!VERIFY(SUCCEEDED(hr)))
return false;
szImeName = bstrImeName;
SysFreeString(bstrImeName);
}
// Encode Chinese words "BaiDu" since our source code file is in ASCII encoding
static const TCHAR szBaidu[] = _T("\u767e\u5ea6");
return (szImeName.Find(szBaidu) != -1);
}
這個邏輯很簡單,就是判斷用戶使用的是不是百度輸入法。這個函數代碼量還是有一些的,但是原理很簡單也很醜陋,獲取輸入法名字,搜索中文“百度”一詞。。。這個和百度輸入法開發人員確認過了,他們保證,他們輸入法名字裏面肯定有百度二字!這個函數的具體分析先放一放,先理一遍思路:
1. 截獲了WM_KEYDOWN,假如是一個VK_PROCESSKEY,那麼就再判斷 --> 是不是輸入法進入了組詞模式?是的話,那麼就是真實的IME,就吞了WM_CHAR,顯示空的輸入框和IME窗口;假如不是,那麼就發送WM_CHAR,讓輸入框正常顯示出來,那個按鍵也自動會發送的輸入框。
2. 對於百度輸入法,首先WM_KEYDOWN裏面如果用戶的輸入不會觸發組詞模式,那麼收到的就不會是VK_PROCESSKEY,那就正常發送WM_CHAR消息;假如是VK_PROCESSKEY,那麼就認爲他肯定進入了組詞模式,從而顯示空的輸入框和候選詞窗口,沒問題~
好,現在就可以仔細看看isCurrentImeBaiduPinyinIme()這個函數了,他裏面取得了操作系統的版本號:
a)假如是win7,那麼直接調用ImmGetDescription()。
b)如果是win8,Imm*** 的API很多就不能用了,因爲Win8開始就使用了TSF(Text Service Framework),TSF非常複雜,使用了一系列的COM接口來操作IME,但是官方的文檔都偏重於講如果去開發一個輸入法,很少提及,外部程序如果使用這些COM接口操作輸入法。但是這篇博客很好的介紹了TSF的架構,這是由微軟Bing輸入法團隊寫的一篇博文:http://blog.csdn.net/mspinyin/article/details/6137709。我也會專門寫一篇博文,講述TSF(在這裏:http://blog.csdn.net/puncha/article/details/13293665)。
看了微軟團隊的博文,只是有了個大概,具體怎麼使用那些COM API呢?費勁千辛萬苦,終於找到了這個帖子:http://social.technet.microsoft.com/Forums/office/zh-CN/002efcfc-8d21-4674-b93b-53c8424d448e/vista-api-immgetdescription?forum=2087,在最後一個評論裏面,有人貼了一段代碼!那段代碼就是用來獲取輸入法名字的!而,我使用的代碼也是基於他寫的,邏輯是:
a)構造ITfInputProcessorProfiles接口。
b)根據當前的langid調用pProfiles->DefaultLanguageProfile(),這是爲了獲取Text Service ID。
c)有了上面取得的Text Service ID,就可以獲取Profile ID了(上面也獲取到了一個,但是沒用,是defaut的,不是active的,真累。。)
d)有了Text Service ID、Profile ID調用pProfiles->GetLanguageProfileDescription()就能取得輸入法名字了!
要注意,對於百度輸入法,Win8的分支代碼在Win7下不能用,會崩潰。同樣,在Win7的分支代碼在Win8下也無效。爲什麼呢?我個人理解,Windows有2個輸入法框架,IMM和TSF,Win7操作系統上,輸入法基本上只實現了IMM框架,所以Imm*** API都正常。而Win8下,那些輸入法都只實現了TSF框架,導致了Imm*** API不能用。但是Bing輸入法是例外, 也正如他們團隊那篇博文所述“按照微軟的說法,TSF會最終取代IMM框架。而微軟拼音基於兼容,功能和性能方面的原因,將這兩個框架都實現了。所以Bing輸入法在Win8還是能通過ImmGetDescription()來獲取輸入法名字。