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()
{
   foreach(InputLanguage lang in InputLanguage.InstalledInputLanguages)
{
      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 Text Service Framework Declaration
//                        from C++ header file
//
//////////////////////////////////////////////////////////////////////////////////////////////
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Security;
 
namespace TSF
{
    [StructLayout(LayoutKind.Sequential)]
    internal struct TF_LANGUAGEPROFILE
    {
        internal Guid clsid;
        internal short langid;
        internal Guid catid;
        [MarshalAs(UnmanagedType.Bool)]
        internal bool fActive;
        internal Guid guidProfile;
    }
 
    [ComImport, SecurityCritical, SuppressUnmanagedCodeSecurity,
Guid("1F02B6C5-7842-4EE6-8A0B-9A24183A95CA"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface ITfInputProcessorProfiles
    {
        [SecurityCritical]
        void Register(); //non-implement!! may be is wrong declaration.
        [SecurityCritical]
        void Unregister(); //non-implement!! may be is wrong declaration.
        [SecurityCritical]
        void AddLanguageProfile(); //non-implement!! may be is wrong declaration.
        [SecurityCritical]
        void RemoveLanguageProfile(); //non-implement!! may be is wrong declaration.
        [SecurityCritical]
        void EnumInputProcessorInfo(); //non-implement!! may be is wrong declaration.
        [SecurityCritical]
        int GetDefaultLanguageProfile(short langid,ref Guid catid,out Guid clsid,out Guid profile);
        [SecurityCritical]
        void SetDefaultLanguageProfile(); //non-implement!! may be is wrong declaration.
        [SecurityCritical]
        int ActivateLanguageProfile(ref Guid clsid, short langid, ref Guid guidProfile);
        [PreserveSig, SecurityCritical]
        int GetActiveLanguageProfile(ref Guid clsid, out short langid, out Guid profile);
        [SecurityCritical]
        int GetLanguageProfileDescription(ref Guid clsid,short langid,ref Guid profile,out IntPtr desc);
        [SecurityCritical]
        void GetCurrentLanguage(out short langid); //non-implement!! may be is wrong declaration.
        [PreserveSig, SecurityCritical]
        int ChangeCurrentLanguage(short langid); //non-implement!! may be is wrong declaration.
        [PreserveSig, SecurityCritical]
        int GetLanguageList(out IntPtr langids, out int count);
       [SecurityCritical]
        int EnumLanguageProfiles(short langid, out IEnumTfLanguageProfiles enumIPP);
        [SecurityCritical]
        int EnableLanguageProfile();
        [SecurityCritical]
        int IsEnabledLanguageProfile(ref Guid clsid, short langid, ref Guid profile, out bool enabled);
        [SecurityCritical]
        void EnableLanguageProfileByDefault(); //non-implement!! may be is wrong declaration.
        [SecurityCritical]
        void SubstituteKeyboardLayout(); //non-implement!! may be is wrong declaration.
    }
 
    [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("3d61bf11-ac5f-42c8-a4cb-931bcc28c744")]
    internal interface IEnumTfLanguageProfiles
    {
        void Clone(out IEnumTfLanguageProfiles enumIPP);
        [PreserveSig]
        int Next(int count, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)]
TF_LANGUAGEPROFILE[] profiles, out int fetched);
        void Reset();
        void Skip(int count);
    }
 
    internal static class TSF_NativeAPI
    {
        public static readonly Guid GUID_TFCAT_TIP_KEYBOARD;
 
        static TSF_NativeAPI()
        {
            GUID_TFCAT_TIP_KEYBOARD = new Guid(0x34745c63, 0xb2f0,
0x4784, 0x8b, 0x67, 0x5e, 0x12, 200, 0x70, 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 profiles;
       if (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);
       }
       return langIDs.ToArray();
}
 
public static string[] GetInputMethodList(short langID)
{
     List<string> imeList = new List<string>();
     ITfInputProcessorProfiles profiles;
    if (TSF_NativeAPI.TF_CreateInputProcessorProfiles(out profiles) == 0)
     {
         try
         {
             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)
                                {
                                    bool enabled;
                                    if (profiles.IsEnabledLanguageProfile(ref langProfile[0].clsid,
 langProfile[0].langid, ref langProfile[0].guidProfile, out enabled) == 0)
                         {
                            if (enabled)
                              imeList.Add(Marshal.PtrToStringBSTR(ptr));
                         }
                      }
                     Marshal.FreeBSTR(ptr);
                 }
              }
          }
       }
       finally
      {
           Marshal.ReleaseComObject(profiles);
       }
    }
    return imeList.ToArray();
 }
上例是節錄自TSFWapper,筆者所設計的TSF Wrapper物件,利用此物件,設計師可以在不了解TSF的情況下,取得輸入法列表及切換輸入法。
在使用上,設計師得先呼叫GetLangIDs函式來取得目前系統所安裝的語言,再針對特定語言呼叫GetInputMethodList函式來取得所安裝的輸入法列表,
如下面的程式片段所示。

 

private short[] langIDs;
………
private void button1_Click(object sender, EventArgs e)
{
     langIDs = TSFWrapper.GetLangIDs();
     if (langIDs.Length > 0)
     {
         string[] list = TSFWrapper.GetInputMethodList(langIDs[0]);
         foreach (string desc in list)
           listBox1.Items.Add(desc);
      }
}
 
下面是此範例於VISTA上運行的畫面。
 
TSF相遇II,切換輸入法
 
 在可以取得輸入法列表後,接下來的工作當然是實作切換輸入法的功能了,在前面的msctf.cs中,ActivateLanguageProfile函式就是作此用途,
同樣的,TSFWrapper物件中也實作了簡單的函式來協助設計師完成此工作。

 

public static bool ActiveInputMethodWithDesc(short langID, string desc)
{
    ITfInputProcessorProfiles profiles;
    if (TSF_NativeAPI.TF_CreateInputProcessorProfiles(out profiles) == 0)
    {
       try
       {
          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)
                        {
                             bool enabled;
                             if (profiles.IsEnabledLanguageProfile(ref langProfile[0].clsid,
langProfile[0].langid, ref langProfile[0].guidProfile, out enabled) == 0)
                             {
                                if (enabled)
                                {
                                    string s = Marshal.PtrToStringBSTR(ptr);
                                    if (s.Equals(desc))
                                       return profiles.ActivateLanguageProfile(ref langProfile[0].clsid,
 langProfile[0].langid, ref langProfile[0].guidProfile) == 0;
                                 }
                              }
                              Marshal.FreeBSTR(ptr);
                         }
                       }
                   }
                }
            }
            finally
            {
                Marshal.ReleaseComObject(profiles);
            }
       }
       return false;
}
使用此函式的方法很簡單,只需傳入欲切換的輸入法名稱及語言(LANGID)即可。
 

 

private void button2_Click(object sender, EventArgs e)
{
    if (langIDs != null)
    {
           if (listBox1.SelectedIndex != -1)
               TSFWrapper.ActiveInputMethodWithDesc(langIDs[0], (string)listBox1.SelectedItem);
     }
 }
 
 
與TSF暫別,取得現行輸入法及關閉輸入法
 
 能切過去,也要能切回來,本文最後的工作就是得將輸入法切回英文輸入,在進入正題前,筆者先介紹TSFWrapper中的另一個函式:GetCurrentInputMethodDesc
此函式會傳回目前系統作用中的輸入法名稱,這有何用呢?一般來說,在設計自動輸入法切換時,會有兩種模式,一是要求使用者選擇一種輸入法做為主要輸入法,
當焦點所在欄位需要輸入中文時,系統自動切換至此輸入法。另一種模式是是不硬性要求使用者選擇輸入法,而是以最近所切換的輸入法為準,在這種模式下,
GetCurrentInputMethodDesc就可以派上用場了。好了,回到正題來,在中文欄位切成中文輸入法,在英文欄位時當然就得切回英數輸入法了,
TSFWrapper提供了此函式。

 

public static bool DeActiveInputMethod(short langID)
{
     List<string> imeList = new List<string>();
     ITfInputProcessorProfiles profiles;
     if (TSF_NativeAPI.TF_CreateInputProcessorProfiles(out profiles) == 0)
    {
        try
        {
             Guid clsid = Guid.Empty;
             return profiles.ActivateLanguageProfile(ref clsid, langID, ref clsid) == 0;
        }
        finally
       {
             Marshal.ReleaseComObject(profiles);
        }
     }
     return false;
 }
 
後記
 
   TSF目前所能取得的資訊相當的少,於google上討論此課題的文章也極其稀少,僅只有MSDN上幾行敘述,希望筆者此篇文章能多少幫助諸位,
少走一些冤枉路,下次再見了!