VC++在Win7和Win8系統下獲得百度輸入法的名字

最近又開始研究輸入法的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);
        }
    }

    。。。。。。
}

上面有一個pView->suppressResendEatenImeChar()別去管他,將來我寫另外一篇博文的時候會用到,主要是爲了修另外一個bug,做的特殊處理,防止一個輸入法字符發送2次。
注意到,我上面用了一個函數pView->isEnterImeCompositionMode(),這個函數的代碼:

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()來獲取輸入法名字。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章