最近又开始研究输入法的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("u767eu5ea6"); 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()来获取输入法名字。