天天看點

VISTA 與輸入法程式介面

原文: VISTA 與輸入法程式介面

VISTA 與輸入法程式介面

文/黃忠成

   近日,我所兼職顧問的公司開始将舊有的Win32 程式及新開發的.NET 應用程式移轉到VISTA 系統上測試,由于我們的應用程式多半是商用套裝軟體,

相當然爾對于以程式切換輸入法的需求是一定存在的,對于客戶來說,在焦點移往該輸入中文的欄位時,由系統自動為其切換适當的輸入法是種便利的設計!

隻是這些原本在Windows XP/2000/2003 上運作的相當正常的程式,到了VISTA 後,卻不約而同出現了同樣的問題,那就是自動切換輸入法的功能全部失效了,

這不隻出現在舊有的Win32 應用程式,連新開發的.NET Framework 2.0 應用程式也無法幸免!當工程師們向我詢問關于此問題的解決辦法時,我直覺的認為,

這可能是VISTA 在輸入法的程式介面上做了變動,也就是舊有的API 已經失去功能,由另外一種介面來取代了!隻是,我毫無頭緒,不知該如何去找出這個

新介面是什麼,更别談說提出一個可以解決此問題的辦法了。我與多數設計師一樣,立刻就打開google ,企圖在搜尋引擎上找到一點蛛絲馬迹,

很不幸的!google 上找不到任何有關此問題的線索,在這種情況下,我想到了.NET Framework 3. 0 ,這是目前最新的.NET Framework版本,或許裡面

已經使用到了這個新的API,但測試的結果仍然是一樣,原本于.NET Framework的Windows Form應用程式中,我們可以利用以下的程式碼來列出系統

中所安裝的輸入法。

public void GetLanguages()

{

   (在InputLanguage.InstalledInputLanguages中的InputLanguage lang)

      textBox1.Text + = lang.Culture.EnglishName +'\ n';

   }

}

基本上,此方法通用于.NET Framework 1.0/1.1/2.0/3.0,在Windows XP/2000/2003上都可以正常運作,但在VISTA下,這個方式隻能列出該系統所安裝的語言,

而非輸入法!事實上,這個物件是利用Windows API:GetKeyboardLayoutList函式來取得輸入法清單,而此函式目前看來,已經無法在VISTA上正常運作了。

既然在Windows Form Framework下無法找到線索,我轉往新的Framework:Windows Presentation Foundation,也就是WPF!這是Windows最新的UI介面,

總該有些線索了吧?答案很令我意外,WPF中雖然也存在着InputLanguageManager物件,但一樣也隻能列出系統所安裝的語言,無法進一步的列出輸入法。

最後!我将腦筋動到了正處于Beta的.Net Framework 3.5上,雖然結果仍然相同,但于其中我發現了一個Framework的蹤迹,那就是TSF(Text Service Framework),

看來!在VISTA中的Imm32.dll(用來管理、切換輸入法介面所在的DLL)所有功能皆已被此Framework完全取代。既然已經找到了一點蛛絲馬迹,接下來就隻要搞清楚

TSF的設計概念及使用方式,就能夠解決當下所遭遇到的問題了。TSF是一組以COM物件組成的Framework,主要目的在提供更具延展性、安全性的語言服務,

與舊有的Imm32.dll以輸入法為中心的設計不同,TSF一開始就設計成可于單一系統中安裝多個語言,而每個語言可以擁有多個輸入法,從此點看來,在以多語言

支援所設計的VISTA環境下,Imm32.dll會失效的理由就不難了解了。好了!這就是前半部的探索過程,現在就讓我們進入問題的核心,TSF所提供的功能相當多,

但目前我們隻需要列出輸入法、切換輸入法這些功能,是以本文就将焦點集中于此,待日後有機會再與讀者們分享TSF其它的運用。

與TSF相遇,列出特定語言下的輸入法

 在現在所能找到的TSF資訊,皆是以C++做為基準所撰寫的,所有範例也都以C++來撰寫的,是以要于.NET中運用TSF的話,首先得先将Windows SDK中所提供

的TSF C++ Header file以P/Invoke方式宣告成.NET語言可用的格式,本文中以C#為例,如下所示:

[msctf.cs]

///////////////////////////////////////////////////////////////////////////////////////////////

// Microsoft文本服務架構聲明

//從C ++頭檔案

//

//////////////////////////////////////////////////////////////////////////////////////////////

使用系統;

使用System.ComponentModel;

使用System.Collections.Generic;

使用System.Text;

使用System.Runtime.InteropServices;

使用System.Security;

命名空間TSF

    [ StructLayout(LayoutKind .Sequential)]

    内部 結構 TF_LANGUAGEEPROFILE

    {

        内部 Guid clsid;

        内部 短 LANGID;

        内部 Guid catid;

        [ MarshalAs(UnmanagedType .Bool)]

        内部 布爾功能;

        内部 Guid guidProfile;

    }

    [ ComImport,SecurityCritical,SuppressUnmanagedCodeSecurity,

Guid (“1F02B6C5-7842-4EE6-8A0B-9A24183A95CA”),

     InterfaceType(ComInterfaceType .InterfaceIsIUnknown)]

    内部 接口 ITfInputProcessorProfiles

        [ SecurityCritical ]

        void Register(); //非執行!可能是錯誤的聲明。

        void Unregister(); //非執行!可能是錯誤的聲明。

        void AddLanguageProfile(); //非執行!可能是錯誤的聲明。

        void RemoveLanguageProfile(); //非執行!可能是錯誤的聲明。

        void EnumInputProcessorInfo(); //非執行!可能是錯誤的聲明。

        int  GetDefaultLanguageProfile(short langid,ref Guid catid,out Guid clsid,out Guid profile);

        void SetDefaultLanguageProfile(); //非執行!可能是錯誤的聲明。

        int ActivateLanguageProfile(ref Guid clsid,short langid,ref Guid guidProfile);

        [ PreserveSig,SecurityCritical ]

        int GetActiveLanguageProfile(ref Guid clsid,out short langid,out Guid profile);

        int GetLanguageProfileDescription(ref Guid clsid,short langid,ref Guid profile,out IntPtrdesc);

        void GetCurrentLanguage(out short langid); //非執行!可能是錯誤的聲明。

        int ChangeCurrentLanguage(short langid); //非執行!可能是錯誤的聲明。

        int GetLanguageList (out IntPtr langids,out int count);

       [ SecurityCritical ]

        int EnumLanguageProfiles(short langid,out IEnumTfLanguageProfiles enumIPP);

        int EnableLanguageProfile();

        int IsEnabledLanguageProfile(ref guid clsid,short langid,ref Guid profile,out bool enabled);

        void EnableLanguageProfileByDefault(); //非執行!可能是錯誤的聲明。

        void SubstituteKeyboardLayout(); //非執行!可能是錯誤的聲明。

    [ ComImport,InterfaceType(ComInterfaceType .InterfaceIsIUnknown),

 Guid(“3d61bf11-ac5f-42c8-a4cb-931bcc28c744”)]

    内部 接口 IEnumTfLanguageProfiles

        無效克隆(輸出 IEnumTfLanguageProfiles enumIPP);

        [ PreserveSig ]

        int Next(int count,[ Out,MarshalAs(UnmanagedType .LPArray,SizeParamIndex = 2)]

TF_LANGUAGEPROFILE [] profiles,out int fetched);

        無效重置();

        無效跳過(int count);

    内部 靜态 類 TSF_NativeAPI

        公共 靜态 隻讀 guid GUID_TFCAT_TIP_KEYBOARD;

        靜态 TSF_NativeAPI()

        {

            GUID_TFCAT_TIP_KEYBOARD = 新的 Guid(0x34745c63,0xb2f0,

0x4784,0x8b,0x67,0x5e,0x12,200x,0x1a,0x31);

        }

        [ SecurityCritical,SuppressUnmanagedCodeSecurity,DllImport(“msctf.dll”)]

        public static extern int TF_CreateInputProcessorProfiles(out ITfInputProcessorProfiles profiles);

OK !我知道,這段程式碼對于不熟悉COM、P/Invoke的讀者而言,就像是無字天書般難懂,不過請放心,我們後面會再撰寫一個Wrapper物件,

簡化使用TSF的過程。在這個程式碼中,有幾個函式值得注意,第一個就是GetLanguageList,她可以列出系統中所安裝的語言,并傳回一個

LANGID型别的陣列,一般來說,預設的語言會排在陣列中的第一個,透過LANGID,我們就能夠呼叫另一個函式:EnumLanguageProfiles

來取得該語言下所安裝的輸入法了,如下例所示:

[TSFWrapper.cs]

public static short [] GetLangIDs()

       List < short > langIDs = new List < short >();

       ITfInputProcessorProfiles配置檔案;

       如果(TSF_NativeAPI .TF_CreateInputProcessorProfiles(out profiles)== 0)

      {

           IntPtr langPtrs;

           int fetchCount = 0;

           if(profiles.GetLanguageList(out langPtrs,out fetchCount)== 0)

           {

               for(int i = 0; i <fetchCount; i ++)

               {

                   short id = Marshal .ReadInt16(langPtrs,sizeof(short)* i);

                   langIDs.Add(ID);

               }

           }

           Marshal .ReleaseComObject(profiles);

       }

       傳回 langIDs.ToArray();

public static string [] GetInputMethodList(short langID)

     List < string > imeList = new List < string >();

     ITfInputProcessorProfiles配置檔案;

    如果(TSF_NativeAPI .TF_CreateInputProcessorProfiles(out profiles)== 0)

     {

         嘗試

         {

             IEnumTfLanguageProfiles enumerator = null ;

             if(profiles.EnumLanguageProfiles(langID,out enumerator)== 0)

             {

                if(enumerator!= null)

                {

                     TF_LANGUAGEPROFILE [] langProfile = new TF_LANGUAGEPROFILE [1];

                     int fetchCount = 0;

                     while(enumerator.Next(1,langProfile,out fetchCount)== 0)

                     {

                                IntPtr ptr;

                                if(profiles.GetLanguageProfileDescription(ref langProfile [0] .clsid,

langProfile [0] .langid,ref langProfile [0] .guidProfile,out ptr)== 0)

                                {

                                    布爾啟用;

                                    if(profiles.IsEnabledLanguageProfile(ref langProfile [0] .clsid,

 langProfile [0] .langid,ref langProfile [0] .guidProfile,out enabled)== 0)

                         {

                            如果(啟用)

                              imeList.Add(Marshal .PtrToStringBSTR(ptr));

                         }

                      }

                     Marshal.FreeBSTR(ptr);

                 }

              }

          }

       最後

    return imeList.ToArray();

 }

上例是節錄自TSFWapper,筆者所設計的TSF Wrapper物件,利用此物件,設計師可以在不了解TSF的情況下,取得輸入法清單及切換輸入法。

在使用上,設計師得先呼叫GetLangIDs函式來取得目前系統所安裝的語言,再針對特定語言呼叫GetInputMethodList函式來取得所安裝的輸入法清單,

如下面的程式片段所示。

私人短 [] langIDs;

………

private void button1_Click(object sender,EventArgs e)

     langIDs = TSFWrapper .GetLangIDs();

     如果(langIDs.Length> 0)

         string [] list = TSFWrapper .GetInputMethodList(langIDs [0]);

         的foreach(字元串遞減的清單)

           listBox1.Items.Add(降序);

      }

下面是此範例于VISTA上運作的畫面。

VISTA 與輸入法程式介面

與TSF 相遇II ,切換輸入法

 在可以取得輸入法清單後,接下來的工作當然是實作切換輸入法的功能了,在前面的msctf.cs中,ActivateLanguageProfile函式就是作此用途,

同樣的,TSFWrapper物件中也實作了簡單的函式來協助設計師完成此工作。

public static bool ActiveInputMethodWithDesc(short langID,string desc)

    ITfInputProcessorProfiles配置檔案;

       嘗試

       {

          IEnumTfLanguageProfiles enumerator = null ;

          if(profiles.EnumLanguageProfiles(langID,out enumerator)== 0)

          {

              if(enumerator!= null)

              {

                    TF_LANGUAGEPROFILE [] langProfile = new TF_LANGUAGEPROFILE [1];

                    int fetchCount = 0;

                    while(enumerator.Next(1,langProfile,out fetchCount)== 0)

                    {

                        IntPtr ptr;

                        if(profiles.GetLanguageProfileDescription(ref langProfile [0] .clsid,

 langProfile [0] .langid,ref langProfile [0] .guidProfile,out ptr)== 0)

                        {

                             布爾啟用;

                             if(profiles.IsEnabledLanguageProfile(ref langProfile [0] .clsid,

langProfile [0] .langid,ref langProfile [0] .guidProfile,out enabled)== 0)

                             {

                                如果(啟用)

                                {

                                    string s = Marshal .PtrToStringBSTR(ptr);

                                    if(s.Equals(desc))

                                       傳回 profiles.ActivateLanguageProfile(參考 langProfile [0] .clsid,

 langProfile [0] .langid,ref langProfile [0] .guidProfile)== 0;

                                 }

                              }

                              Marshal.FreeBSTR(ptr);

                       }

                   }

                }

            }

            最後

            {

                Marshal .ReleaseComObject(profiles);

       傳回 false ;

使用此函式的方法很簡單,隻需傳入欲切換的輸入法名稱及語言(LANGID)即可。

private void button2_Click(object sender,EventArgs e)

    如果(langIDs!= null)

           if(listBox1.SelectedIndex!= -1)

               TSFWrapper .ActiveInputMethodWithDesc(langIDs [0],(string)listBox1.SelectedItem);

     }

與TSF暫别,取得現行輸入法及關閉輸入法

 能切過去,也要能切回來,本文最後的工作就是得将輸入法切回英文輸入,在進入正題前,筆者先介紹TSFWrapper 中的另一個函式:GetCurrentInputMethodDes c ,

此函式會傳回目前系統作用中的輸入法名稱,這有何用呢?一般來說,在設計自動輸入法切換時,會有兩種模式,一是要求使用者選擇一種輸入法做為主要輸入法,

當焦點所在欄位需要輸入中文時,系統自動切換至此輸入法。另一種模式是是不硬性要求使用者選擇輸入法,而是以最近所切換的輸入法為準,在這種模式下,

GetCurrentInputMethodDesc就可以派上用場了。好了,回到正題來,在中文欄位切成中文輸入法,在英文欄位時當然就得切回英數輸入法了,

TSFWrapper提供了此函式。

public static bool DeActiveInputMethod(short langID)

     如果(TSF_NativeAPI .TF_CreateInputProcessorProfiles(out profiles)== 0)

        嘗試

             Guid clsid = Guid .Empty;

             傳回 profiles.ActivateLanguageProfile(ref clsid,langID,ref clsid)== 0;

        最後

       {

             Marshal .ReleaseComObject(profiles);

        }

     傳回 false ;

後記

   TSF 目前所能取得的資訊相當的少,于google 上讨論此課題的文章也極其稀少,僅隻有MSDN 上幾行叙述,希望筆者此篇文章能多少幫助諸位,

少走一些冤枉路,下次再見了!

範例下載下傳: 

http://code6421.myweb.hinet.net/Old/VistaIME/VistaIMEHelperNet.zip