読者です 読者をやめる 読者になる 読者になる

Windows の IME を変換エンジンとして使う

f:id:NyaRuRu:20131218051110p:plain
プロセス分離型の IME の開発に携わった以上,一度は試してみたいと思っていた奴,の基礎実証実験っぽいのをやってみた.Windows向け IME を (とくに個人規模で) 作っている人にはもしかしたら役に立つかも.

テーマ

メインテーマは,Windows で IME を実装するとして,バックグラウンドで別 IME を有効化し,その IME に対してクエリを投げ,返ってきた結果を利用するための技術的な枠組みについて.ここではプロセスモデル的に Windows で可能かどうかという点のみを考える.

大まかな流れ

今回試した手法では,Windows 8 で TSF に追加された ITfFnSearchCandidateProvider という仕組みを活用する.このインターフェイスは,複雑なことはできない反面,1) 文字列を投げて文字列のリストが返ってくるというシンプルな仕組みである,2) ステートを持たず,IME でユーザーが入力中かどうかに関わらず自由に使える,という点で大変使いやすい.ただしすべての IME が実装しているわけではない.これについては後述.
また,このインターフェイスには "Windows 8 [desktop apps only]" と注釈がついている.ここで,プロセス分離型の IME 実装を前提とするのが生きてくる.プロセス分離型の実装では,好きにデスクトッププロセスと連携できるのでこれは特に問題にはならない.

具体的な流れ

以下の手順で,キーボードフォーカスを持たないバックグラウンドプロセス内で,ITfFnSearchCandidateProvider をサポートした IME を変換エンジンとして使用できる.

  1. バックグラウンドスレッドを作る
  2. スレッド内で COM を初期化する (STA 推奨)
  3. ITfInputProcessorProfileMgr.ActivateProfile を使用して,使用したい IME を有効化する.
  4. ITfThreadMgr2.ActivateEx を呼んで IME に初期化処理を行わせる.
  5. ITfThreadMgr2.GetFunctionProvider に,対象 IME の CLSID を渡して ITfFunctionProvider を取得
  6. GetFunctionProvider.GetFunction 経由で ITfFnSearchCandidateProvider を取得
  7. ITfFnSearchCandidateProvider.GetSearchCandidates で任意のクエリを行う

罠ポイント

上記手順の 3 が問題. Windows 8 以降はデスクトッププロセス間で IME 関係の設定を共有するようになったため,うかつに別 IME を選択すると他スレッド/他プロセスにも影響が及んでしまう.これについては, API を使って一時的にこの挙動に関する設定そのものを変更することで回避できそう.具体的には,SystemParametersInfo API に SPI_GETTHREADLOCALINPUTSETTINGS/ SETTHREADLOCALINPUTSETTINGS を指定して,以前の「スレッドごとに IME 関係の設定を保持」に変更する.変更は永続的である必要はなくて,IME を変更するごく短い間だけで十分なようだ.というわけで先ほどの手順をもう少し改良する.

  1. バックグラウンドスレッドを作る
  2. スレッド内で COM を初期化する (STA 推奨)
  3. 「スレッドごとに IME 関係の設定を保持」が有効でなければ SETTHREADLOCALINPUTSETTINGS を使って有効にする
  4. ITfInputProcessorProfileMgr.ActivateProfile を使用して,使用したい IME を有効化する.
  5. ITfThreadMgr2.ActivateEx を呼んで IME に初期化処理を行わせる.
  6. ITfThreadMgr2.GetFunctionProvider に,対象 IME の CLSID を渡して ITfFunctionProvider を取得
  7. GetFunctionProvider.GetFunction 経由で ITfFnSearchCandidateProvider を取得
  8. 「スレッドごとに IME 関係の設定を保持」を変更していれば,SETTHREADLOCALINPUTSETTINGS で元に戻す
  9. ITfFnSearchCandidateProvider.GetSearchCandidates で任意のクエリを行う

実際には「スレッドごとの IME 設定」を元に戻すのはもう少し手前でも問題ないかもしれない.
また,ユーザーが「アプリウィンドウごとに異なる入力方式を設定する」を有効にしている場合,GETTHREADLOCALINPUTSETTINGS は TRUE を返す.この場合はスレッドごとに異なる IME を有効化できるため何もしなくてよい.

応用

上の罠ポイントさえ回避できれば,複数のバックグラウンドスレッドを用意し,それぞれに別 IME をロードして構わない.つまり,複数の IME に対して同時にクエリを行うことができる.

ITfFnSearchCandidateProvider をサポートしている IME はどれぐらいあるのか?

手元の Windows 8.1 環境でしらべたところ,MS-IME 日本語版と ATOK 2013 はサポートしている.Google 日本語入力 / OSS Mozc は1.12.1599.102 の時点で未サポート.以下,実験したい人のための GUID 一覧.

{
  name: "MS-IME Japanese (Windows 8.1)",
  langid: 0x0411,
  clsid: "03B5835F-F03C-411B-9CE2-AA23E1171E36",
  profile: "A76C93D9-5523-4E90-AAFA-4DB112F9AC76",
},
{
  name: "ATOK 2013",
  langid: 0x0411,
  clsid: "E7602D3E-204C-4662-B92F-78DF0DE5752D",
  profile: "3C4DB511-189A-4168-B6EA-BFD0B4C85615",
},
{
  name: "Google Japanese Input",
  langid: 0x0411,
  clsid: "D5A86FD5-5308-47EA-AD16-9C4EB160EC3C",
  profile "773EB24E-CA1D-4B1B-B420-FA985BB0B80D",
},
{
  name: "OSS Mozc",
  langid: 0x0411,
  clsid: "10A67BC8-22FA-4A59-90DC-2546652C56BF",
  profile: "186F700C-71CF-43FE-A00E-AACB1D9E6D3D",
},
{
  name: "Corvus-skk",
  langid: 0x0411,
  clsid: "EAEA0E29-AA1E-48ef-B2DF-46F4E24C6265",
  profile: "956F14B3-5310-4cef-9651-26710EB72F3A",
},

まとめ

Windows 8.1 環境で ITfFnSearchCandidateProvider を利用すると,MS-IME 日本語版と ATOK 2013 を(同時に)フォールバックエンジンとして使う IME が作成可能.

速度とか所感

MS-IME Japanese は大体 10 msec 以内で返ってくるのでサジェストでも大丈夫かも.ATOK 2013 はクエリが返ってくるまで 100 msec 以上かかっていて,リアルタイムのサジェストに使うのはちょっときついかも.

サンプルコード

次のコードは,現在使用中の IME からのみ ITfFnSearchCandidateProvider を取得することで色々処理を簡略化したもの.起動後に何か入力すると,その入力を ITfFnSearchCandidateProvider.GetSearchCandidates に渡した結果が返ってくる.空行入力で終了.
ビルドには要 NuGet.Package Manager から TSF.SearchCandidateProvider を追加.

PM > Install-Package TSF.SearchCandidateProvider
using System;
using System.Linq;
using TSF;

static class Program {
  static void Main(string[] args) {
    using (var provider = SearchCandidateProvider.FromCurrentProfile().Result) {
      if (provider == null) {
        Console.WriteLine("No provider found.");
        return;
      }
      Console.WriteLine("{0} is found", provider.Profile.Name);
      while (true) {
        var line = Console.ReadLine();
        if (string.IsNullOrEmpty(line)) {
          return;
        }
        var result = provider.GetSearchCandidates(line).Result;
        Console.WriteLine("Elapsed time: {0} msec", result.Elaplsed.TotalMilliseconds);
        result.Candidates.ToList().ForEach(s => Console.WriteLine("  " + s));
        Console.WriteLine();
      }
    }
  }
}

ビルドするのは面倒なのでビルド済みファイルが欲しいという人はこちら.
http://d.hatena.ne.jp/NyaRuRu/files/SearchCandidateProviderSample.zip

TSF.SearchCandidateProvider のコードが見たい人はこちらから.
NyaRuRu/TSF-SearchCandidateProvider · GitHub

類似研究

アレ用の何か
ITfFnReconversion を使う例.