.Net Framework における IME

Last Updated 2011/09/21


私は C# でテキストエディタコントロールを作る計画を持っていますが、その過程で IME について研究してみました。日本語環境において、IME "Input Method Editor" は無視できない存在だからです。しかし、.Net Framework の IME に関係する機能はほぼゼロに等しいといってよい状況ですので、必然的に Windows の機能を直接呼び出すことになります。

このページでは、IME に関係する Windows API 関数および COM インターフェースの使い方を中心として解説します。なお、このページで紹介するサンプルコードは C# だけですが、Visual Basic な人にも参考になると思います。


Microsoft IME 2003

Windows Vista に搭載されている IME エンジンは Microsoft IME 2003 です(ちなみに、Windows 7 では特に名前は付いていませんが、バージョンは 10.1 です)。ただし、エンジンの名前としては Office IME 2003 と呼ぶのが正しいようで、もともと Office 2003 に採用されたものです。つまり、Windows Vista では Office IME 2003 と同じエンジンを使っていて、それを Microsoft IME と呼ぶことにしたのですが、習慣的に Microsoft IDE 2003 と呼ぶということのようです。事実、Visual Studio 2005 のヘルプの中に "Microsoft IME 2003, Japanese Version" という項目があります。

Note 不思議なことに、Visual Studio 2008 のヘルプの中に、その項目がなくなっていますが、代わりに、"Internationalization" という項目が追加されました。その中の一部として、IME の項目があります。
また、Visual Studio 2010 では "Internationalization for Windows Applications" という項目の下に、"InputMethod Manager" があります。

ところで、IME の最新バージョンは Microsoft IME 2007 ですが、これも正しくは Office IME 2007 と呼ぶようです。つまり、Office 製品をインストールしていない環境では使えないということになります。Windows と Office 製品との IME のバージョンが必ずしも一致しない点には留意しておかなければなりません。

.Net Framework の IME に関係する機能

IME に関係する .Net Framework の機能としては、Control クラスまたはそれを継承するクラスの ImeMode プロパティ、および、それに関係するメソッドとイベントだけです。ImeMode プロパティは IME をオンまたはオフにする、あるいは、「ひらがな」モードとか、「カタカナ」モードを切り替えるときに使います。ImeMode プロパティについてはこれ以上の説明は不要でしょう。

TSF "Text Services Framework"

Windows XP SP1 以後、TSF が追加されました。従来の IME に代わる次世代のテキスト変換機能を持つものと Microsoft は言っています。

WEB サイトをうろついていると、Windows Vista になってから IME の動作がおかしいとの記述をたくさん見ることができますが、実は Windows Vista の IME は TSF のラップでしかないことにトラブルの原因があるようです。ラップとは従来の IME 関係の API 関数を呼び出すと、TSF の該当する機能を呼び出すという意味です。もはや IME ではなく、TSF を使えという意味なのでしょうか。

たとえば、次のトピックで扱う「入力した漢字のふりがなを取得する」ときに、ImmGetConversionList 関数を呼び出している人がいるようですが、Windows Vista でこの関数を呼び出してもエラーにはならないものの正常には動作しません。

さて、TSF とは何かについて説明しましょう。

IME はキーボードからのテキストの入力だけに対応していますが、テキストを入力する手段はキーボードだけに限りません。たとえば、音声入力という手段もあります。スクリーンから直接読み込むスクリーンリーダーもあります。ハンドライティング、つまり、手書き入力という手段をサポートするツールもすでにあります。

このように、従来の IME では間に合わなくなってきたことが TSF 導入のキッカケとなったようです。ただし、いまだ発展途上のテクノロジといっていいでしょう。日本人には IME はなじみのあるテクノロジですが、アメリカ人はいままで使っていなかったのですから TSF の開発者も直感的な理解がとぼしいと考えられるからです。ヒョットすると、数年後にはガラっと姿を変える可能性もあるなと思っています。.Net Framework 1.0/1.1 と .Net Framework 2.0 とは似て非なる存在であることを思い出してください。

キーボードのレイアウト

IME 関係の API 関数を使う場合、キーボードレイアウトのハンドルを要求されます。そのための API 関数のプロトタイプは以下のとおりです。

  [DllImport("User32.dll")]
  public static extern IntPtr GetKeyboardLayout(int idThread);

idThread はスレッドの ID を指定するものですが、現在のスレッドでよければ 0 を指定します。つまり、通常はキーボードレイアウトのハンドルは次のコードで取得できます。

  IntPtr hKL = GetKeyboardLayout(0);

IME を扱う場合に何でキーボードレイアウトのハンドルが必要なのかといぶかしく思う人もいると思いますが、これには歴史的な経緯が関係しています。

ところで、キーボードレイアウトのハンドルと呼びましたが、現在では「入力ロケール識別子」という呼び方に代わっています。したがって、本来は GetInputLocaleID あたりに関数の名前を変えるべきところですが、過去の遺産との互換性のためそのままになっているというわけです。昔はキーボードレイアウトの中に現在のロケール情報というべきものが含まれていましたが、現在はより複雑な情報が必要になったことで、この関数の使い方は以前のままですが、戻り値の内容は相当違うものであると理解して置いてください。

入力した漢字のふりがなを取得する(その1)

さて、ここからが IME の操作に関係する記事になるのですが、もっとも需要の多い機能は入力した漢字のふりがなを取得する方法です。たとえば、氏名の入力を要求するとき、漢字だけでなく、その読みもほしいものです。そのとき、両方を入力してもらうのは気が引けるので、漢字を入力すると自動的にふりがなを取得できると便利です。

「TSF "Text Services Framework"」の項でチョット触れましたが、これを実現するために ImmGetConversionList 関数を使う人がいます。この関数を使っても不可能ではありませんが、そもそも使うべき関数を間違っています。ImmGetCompositionString 関数ならズバリな解答を得ることができます。しかも、Windows Vista でも問題なく使えます。

なお、ここでは .Net Framework 的な解決策を提示するという意味で、入力した漢字の読みを返すコントロールを作ることにします。コントロール化が絶対に必要というわけではありませんが、このほうがコードの再利用性がよいと考えました。

以下は、TextBox クラスから派生するコントロールです。コードの全体をコピーし、ファイル名を CYomiTextBox.cs とでもしてください。コントロールをテストするプロジェクトにファイルを追加し、ビルドしたあと、フォームにコントロールをはりつけてください。なお、プロジェクトには Microsoft.VisualBasic.dll への参照を追加しなければなりません。

フォームにコントロールを貼り付けたあと、CompositionCompleted イベントハンドラに以下のコードを書いてください。textBox1 には入力した漢字の読みを全角カタカナで、textBox2 にはひらがなの読みを、textBox3 には半角カタカナで表示します。

private void yomiTextBox1_CompositionCompleted(object sender, emanual.Control.CompositionCompletedEventArgs e)
{
  textBox1.Text = e.Katakana;
  textBox2.Text = e.Hiragana;
  textBox3.Text = e.HanKana;
}
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Microsoft.VisualBasic; // StrConv メソッド(プロジェクトに Microsoft.VisualBasic.dll を追加すること)

namespace emanual.Control
{
  public class YomiTextBox : TextBox
  {
    private const int WM_IME_COMPOSITION = 0x010F;
    private const int GCS_RESULTREADSTR = 0x0200;

    // デリゲートの宣言
    public delegate void CompositionCompletedEventHandler(object sender, CompositionCompletedEventArgs e);

    // イベントハンドラの宣言
    public event CompositionCompletedEventHandler CompositionCompleted;

    [DllImport("Imm32.dll")]
    private static extern int ImmGetContext(IntPtr hWnd);
    [DllImport("Imm32.dll")]
    private static extern int ImmGetCompositionString(int hIMC, int dwIndex, StringBuilder lpBuf, int dwBufLen);
    [DllImport("Imm32.dll")]
    private static extern bool ImmReleaseContext(IntPtr hWnd, int hIMC);

    //-------------------------------------------------------------------------------------
    // コンストラクタ
    public YomiTextBox()  : base()
    {
    }

    //-------------------------------------------------------------------------------------
    protected override void WndProc(ref Message m)
    {
      if (CompositionCompleted != null && m.Msg == WM_IME_COMPOSITION)
      {
        if (((int)m.LParam & GCS_RESULTREADSTR) == GCS_RESULTREADSTR)
        {
          int hImc = ImmGetContext(this.Handle);

          // 読みの文字数を取得する
          int size = ImmGetCompositionString(hImc, GCS_RESULTREADSTR, null, 0);

          StringBuilder sb = new StringBuilder(size);
          sb.Append(new string(' ', size)); // 受け取る文字列の長さに設定する(これがないと 1 文字だけの時にゴミが入るから)

          // 読みを取得する
          ImmGetCompositionString(hImc, GCS_RESULTREADSTR, sb, sb.Length);

          ImmReleaseContext(this.Handle, hImc);

          CompositionCompletedEventArgs args = new CompositionCompletedEventArgs(sb.ToString());

          // CompositionCompleted イベントを発生させる
          CompositionCompleted(this, args);
        }
      }

      base.WndProc(ref m);
    }
  } // end of YomiTextBox class

  //**************************************************************************************
  // CompositionCompletedEventArgs class(文字の変換を終了したときに発生するイベントのデータ)
  //**************************************************************************************
  [Serializable]
  public class CompositionCompletedEventArgs : EventArgs
  {
    private string FHanKana;  // 半角カタカナ
    private string FKatakana; // 全角カタカナ
    private string FHiragana; // 全角ひらがな

    public string HanKana { get { return FHanKana; } }
    public string Katakana { get { return FKatakana; } }
    public string Hiragana { get { return FHiragana; } }

    public CompositionCompletedEventArgs(string s)
    {
      FHanKana = s;
      FKatakana = Strings.StrConv(s, VbStrConv.Wide, 0); // 半角カナを全角カナに変換する
      FHiragana = Strings.StrConv(FKatakana, VbStrConv.Hiragana, 0); // カタカナをひらがなに変換する
    }

  } // end of CompositionCompletedEventArgs class
} // end of namespace

入力した漢字のふりがなを取得する(その2)

後述する「IME の再変換」とも関係があるので、あえて取り上げました。「入力した漢字のふりがなを取得する(その1)」では漢字に変換したタイミングを捉えてその読みを取得しますが、タイミングに関係なく、読みを取得する方法を説明します。

これを実現するには COM インターフェースの IFELanguage インターフェース(正確には IFELanguage2 インターフェース)を利用します。コードの再利用性を考え、クラスにしてみました。以下のコードをファイル(CImeLanguage.cs とでもしてください)にコピーしてください。

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

namespace emanual.IME
{
  public class ImeLanguage
  {
    private bool FInitialized = false;

    private const int S_OK = 0;
    private const int CLSCTX_LOCAL_SERVER = 4;
    private const int CLSCTX_INPROC_SERVER = 1;
    private const int CLSCTX_INPROC_HANDLER = 2;
    private const int CLSCTX_SERVER = CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER;
    private const int FELANG_REQ_REV = 0x00030000;
    private const int FELANG_CMODE_PINYIN = 0x00000100;
    private const int FELANG_CMODE_NOINVISIBLECHAR = 0x40000000;

    [DllImport("ole32.dll")]
    private static extern int CLSIDFromString([MarshalAs(UnmanagedType.LPWStr)] string lpsz, out Guid pclsid);

    [DllImport("ole32.dll")]
    private static extern int CoCreateInstance([MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
        IntPtr pUnkOuter, uint dwClsContext, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr rpv);

    [DllImport("ole32.dll")]
    private static extern int CoInitialize(IntPtr pvReserved);

    [DllImport("ole32.dll")]
    private static extern int CoUninitialize();

    //-------------------------------------------------------------------------------------
    // コンストラクタ
    public ImeLanguage()
    {
      int res = CoInitialize(IntPtr.Zero);

      if (res == S_OK)
        FInitialized = true;
    }

    public void Dispose()
    {
      if (FInitialized)
      {
        CoUninitialize();
        FInitialized = false;
      }
    }

    // デストラクタ
    ~ImeLanguage()
    {
      if (FInitialized)
        CoUninitialize();
    }

    public string GetYomi(string str)
    {
      string yomi = String.Empty;
      Guid pclsid;
      int res;

      // 文字列の CLSID から CLSID へのポインタを取得する
      res = CLSIDFromString("MSIME.Japan", out pclsid);

      if (res != S_OK)
      {
        this.Dispose();
        return yomi;
      }

      Guid riid = new Guid("019F7152-E6DB-11D0-83C3-00C04FDDB82E ");
      IntPtr ppv;
      res = CoCreateInstance(pclsid, IntPtr.Zero, CLSCTX_SERVER, riid, out ppv);

      if (res != S_OK)
      {
        this.Dispose();
        return yomi;
      }

      IFELanguage language = Marshal.GetTypedObjectForIUnknown(ppv, typeof(IFELanguage)) as IFELanguage;
      res = language.Open();

      if (res != S_OK)
      {
        this.Dispose();
        return yomi;
      }

      IntPtr result;

      res = language.GetJMorphResult(FELANG_REQ_REV, FELANG_CMODE_PINYIN | FELANG_CMODE_NOINVISIBLECHAR,
              str.Length, str, IntPtr.Zero, out result);

      if (res != S_OK)
      {
        this.Dispose();
        return yomi;
      }

      yomi = Marshal.PtrToStringUni(Marshal.ReadIntPtr(result, 4), Marshal.ReadInt16(result, 8));

      language.Close();

      return yomi;
    }

  } // end of ImeLanguage class

  //**************************************************************************************
  // IFELanguage Interface(メソッドの実装はランタイムの中にあるので、実装は不要)
  //**************************************************************************************
  [ComImport]
  [Guid("019F7152-E6DB-11D0-83C3-00C04FDDB82E")]
  [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  internal interface IFELanguage
  {
    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    int Open();
    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    int Close();
    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    int GetJMorphResult(uint dwRequest, uint dwCMode, int cwchInput,
        [MarshalAs(UnmanagedType.LPWStr)] string pwchInput, IntPtr pfCInfo, out IntPtr ppResult);
  } // end of IFELanguage Interface

} // end of namespace

このクラスの使い方を説明します。フォームに button1、textBox1、textBox2 を配置します。実行後、textBox1 に適当な漢字を入力して、button1 をクリックすると、textBox2 に漢字の読みを表示します。読みは漢字を入力したときの読みではなく、IFELanguage インターフェースが分析した結果を戻します。たとえば、読みの「ただし」で漢字変換し、textBox1 に「忠」を入力すると、「ただし」ではなく、「ちゅう」が戻ります。このように、漢字 1 文字の場合は期待する読みを返さないケースが多いようです。もっとも、こんな使い方をする人はいないと思いますが。

private void button1_Click(object sender, EventArgs e)
{
  emanual.IME.ImeLanguage lang = new emanual.IME.ImeLanguage();

  textBox2.Text = lang.GetYomi(textBox1.Text);

  lang.Dispose();
}

System.Windows.Input 名前空間

.Net Framework 3.0 に追加された System.Windows.Input 名前空間の中に InputMethod クラスなるものがあることに気付きました。もしやと思って調べたところ、IME に関係するクラスであることが分かりました。

Visual Studio 2008 でさっそく WPF アプリケーションプロジェクトを起こして、InputMethod クラスの ShowConfigureUI メソッドをテストしたところ、「IME プロパティウインドウを表示する」で紹介するダイアログボックスが表示されました。

これは期待できると考え、次に Windows フォームアプリケーションのプロジェクトを作ってテストしたところ、うまくいきません。つまり、以下のコードを実行しても null が戻るからです。もちろん、WPF アプリケーションでは正常に動作します。

  InputMethod method = InputMethod.Current;

どうしてなんでしょうね。ここまできていて動作するアプリケーションの種類を制限する意味が理解できません。あるいは、何か手があるのでしょうか。

特定の文字だけを入力可能とするテキストボックス

IME に関係する情報を得ようと、ある WEB サイトの横を通り過ぎようとしたとき、ひらがなだけを入力可能なテキストボックスコントロールを作ろうとしているが、どうすればいいかとの質問を見つけました。投稿者は [漢字変換] キーの入力をキャンセルしたいができないで困っているということです。

Note ちなみに、[漢字変換] キーを押したとき、WM_IME_REQUEST が飛んできます。これを処理すれば、[漢字変換] キーの操作をキャンセルすることができます。

Microsoft MVP の何人かが意見を投稿していましたが、そんなにたいそうなことでもないのではと思いながら通り過ぎました。しかし、解決策を示さなければと思い、考えてみました。以下のコードでどうでしょうか。IME のオン/オフあるいは設定にかかわらず、指定の文字以外の入力を阻止することができます。

private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
  if (((e.KeyChar < 'ぁ') || (e.KeyChar > 'を')) && (e.KeyChar != (int)Keys.Back))
    e.Handled = true;
}

'ぁ'(小さな「あ」です) と 'ん' との間にある文字を除く、つまり、ひらがなでない文字の場合は無視します。ただし、これでは不完全ですので、[Backspace] も入力可能にしておくほうがいいでしょう。[Delete] キーは何もしなくても使えるので、ここで処理する必要はありません。

このテクニックを使うと、入力を許可する文字を任意に指定することができます。たとえば、数字と英文字の "abc"、ひらがなの "あいう" だけを許可する時はこんな感じです。

private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
  string s = "0123456789abcあいう";

  if (s.IndexOf(e.KeyChar) < 0)
    e.Handled = true;
}

IME のプロパティウインドウを表示する

IME のプロパティウインドウとは下図のようなものです。

ImeProperty

このダイアログボックスを表示する API 関数は ImmConfigureIME 関数です。

  [DllImport("imm32.dll")]
  public static extern bool ImmConfigureIME(IntPtr hKL, IntPtr hWnd, int dwMode, IntPtr lpData);

dwMode には以下の 3 つの定数を指定できることになっているのですが、Windows Vista では IME_CONFIG_GENERAL と IME_CONFIG_SELECTDICTIONARY とは同じ動作をします。つまり、このダイアログボックスを以前に開いたときのタブを再度開きます。開くタブを指定することはできません。

  public const int IME_CONFIG_GENERAL = 1;
  public const int IME_CONFIG_REGISTERWORD = 2;
  public const int IME_CONFIG_SELECTDICTIONARY = 3;

したがって、このダイアログボックスを表示するコードは以下のとおりとなります。

private void button1_Click(object sender, EventArgs e)
{
  IntPtr hKL = GetKeyboardLayout(0); // おきまりの手順ですね

  // dwMode に IME_CONFIG_SELECTDICTIONARY を指定しても動作は同じですから、ここでは 1 を設定しました
  ImmConfigureIME(hKL, this.Handle, 1, IntPtr.Zero);
}

IME の単語/用例を登録するダイアログボックスを表示する

以下のダイアログボックスを表示する手順は IME のプロパティウインドウを表示するときと同じ API 関数を使用しますが、dwMode には IME_CONFIG_REGISTERWORD を指定します。

ImeDic

private void button1_Click(object sender, EventArgs e)
{
  IntPtr hKL = GetKeyboardLayout(0); // おきまりの手順です

  ImmConfigureIME(hKL, this.Handle, IME_CONFIG_REGISTERWORD, IntPtr.Zero);
}

IME の再変換

Windows 付属の NotePda.exe を起動し、適当な文字を入力したあと、入力済みの漢字を選択状態にして、[漢字変換] キーを押すと、下図のように再変換を可能とするポップアップメニューが表示されます。下図の場合は、「日本」を選択状態にしています。

Reconvert

つまり、変換済みの漢字を再変換するチャンスができるというわけです。

上図は、NotePad.exe の例ですが、.Net Framework の TextBox コントロールあるいは RichTextBox コントロールにも同じ機能があります。したがって、テキストボックスコントロールを自作する場合でもなければ、この機能を実装しなければならないケースはないでしょう。

さて、私もそうですが、.Net Framework でテキストエディタコントロールを作ろうとしている人はたくさんいると思います。そこで、IME の再変換を実現するテクニックを公開したいとは考えているのですが、思ったより簡単ではありません。というのは、上図を見ても分かるように、再変換ウインドウが表示される位置はカーソルの位置と関係があると考えられるからです。カーソルの位置を確かにするにはフォーカスを持つ必要がありますので、結局は Control クラスから派生するコントロールが必要となります。

テキストエディタコントロールは比較的複雑なコントロールです。したがって、どんなに簡単なものでも一人前に作ろうとするとかなり面倒です。また、この項目に関心を持った人はテキストエディタコントロールの作成の知識が多少なりともあると考え、ポイントだけを書くことにしました。

まず、ユーザーが [漢字変換] キーを押して、再変換をしようとしたタイミングを捉えるには Windows メッセージを捕まえることになります。

protected override void WndProc(ref Message m)
{
  if (m.Msg == WM_IME_REQUEST)
  {
    if ((int)m.WParam == IMR_RECONVERTSTRING)
    {
      this.DoReconversion(ここに再変換の対象となる読みを設定する); // 漢字ではなく、ふりがなのほうです
    }
  }

  base.WndProc(ref m);
}

以下コードは、指定の読みに対する変換候補をリストアップする再変換ウインドウを表示します。エラーチェックなどのコードは一切含みませんので、これが模範的なコードとは考えないでくださいね。また、API 関数のや定数名の説明を省略しましたが、この項目に興味のある人はすでに分かっているか、調べる手段を知っていると思います。

// s : 再変換する読み
private void DoReconversion(string s)
{
  int dwConversion = 0, dwSentence = 0;

  IntPtr hImc = ImmGetContext(this.Handle);

  ImmSetOpenStatus(hImc, true);

  ImmGetConversionStatus(hImc, ref dwConversion, ref dwSentence);
  dwConversion &= ~IME_CMODE_NOCONVERSION;

  ImmSetConversionStatus(hImc, dwConversion, dwSentence);

  ImmSetCompositionString(hImc, SCS_SETSTR, s, Encoding.Default.GetByteCount(s), IntPtr.Zero, 0);

  ImmNotifyIME(hImc, NI_COMPOSITIONSTR, CPS_CONVERT, 0);
  ImmNotifyIME(hImc, NI_OPENCANDIDATE, 0, 0);

  ImmReleaseContext(this.Handle, hImc);
}

−以上−