Text Service Frameworkに関するメモ

 諸事情でWindowsの文字入力について調べたのだが、専門用語が多くて理解に時間がかかったので、数ヶ月後の自分のためにメモしておく。
 Text Service Framework(以下TSF)は汎用的な文字入力フレームワークで、キーボードだけではなく、音声認識や手書き入力などもサポートできるフレームワークである。
 TSFを考える時には登場人物が3つある。アプリケーション、テキストサービス、テキストサービスマネージャの3種類である。アプリケーションが普通のアプリケーションデベロッパが開発するもの、テキストサービスがかな漢字変換エンジンのベンダーなどが開発するものである。テキストサービスマネージャはアプリケーションとテキストサービスを仲介する。アプリケーションとテキストサービスが直接通信することはなく、必ずテキストサービスマネージャを通す(らしい)。以下、いくつか用語を箇条書きする。

  • アンカーはテキストのある場所を指し示すオブジェクト。アンカー(碇)の名の通り、何文字目、みたいな数字ではなく、テキスト中の文字そのものに対するポインタである。
  • レンジはテキストの一部分を指し示すオブジェクトであり、開始アンカーと終了アンカーでなりたつ。
  • キャレットは開始アンカーと終了アンカーが指し示す位置が同じであるようなアンカーである。
  • コンポジション:入力中のテキストがまだ変更されうる可能性にある状態。かな漢字変換エンジンだと未確定文字列がある状態がこれにあたる。
  • シンク:イベントを受信するもの (COM用語?)
  • ソース:イベントを送信するもの (COM用語?)

エディットセッションとドキュメントのロック

 TSFでは、テキストサービスがアプリケーションで編集中のテキスト(テキストという単語が出現しすぎて混乱の元なので、以下ではドキュメントと言い換える)を参照したり、書き換えたりすることができる。このためにテキストストアという概念があり、ドキュメントはテキストストアを通じてテキストサービスから参照・編集できる。ただ、テキストサービスが常にドキュメントを参照とか編集ができるというわけではなく、何らかの操作を行うためにはアプリケーション側に対してそのドキュメントのロックを要求する必要がある。これは、アプリケーションとテキストサービスとが概念的には完全に独立したものであり、どのようなタイミングでどんな動作が起こるかの自由度が高いためである。同時にドキュメントを編集したりすることを防ぐためにロックが必要となる。
 ドキュメントをロックするために、エディットセッションという概念がある。テキストサービスがアプリケーションにエディットセッションをリクエストし、許可されるとコールバック関数が呼ばれるので、そこでテキスト操作を行う。テキストサービスはこのコールバック関数以外からドキュメントを編集してはいけない(できない?)。
 前述の通り、アプリケーションはテキストサービスに対し、テキストストアを通じてドキュメントの参照と編集を行わせる事になっている。このために、アプリケーションはITextStoreACPかITextStoreAnchorのどちらかのインターフェースを実装する必要がある。これらのインターフェースにはrequestLockというメソッドがついており、テキストサービスがドキュメントのロックを要求した場合には、このメソッドが呼ばれる。
 ここまでの段階で既に、テキストストアとかテキストサービスとか、テキストと名前のつくものが多すぎて読者は混乱しているものだと思われる。テキストサービスを変換エンジンに置換してから読むと多少は概念を把握しやすいかもしれない。

エディットコンテキストとエディットセッションとドキュメントのロック

 エディットコンテキストはテキストサービスマネージャが実装するインターフェースで、このコンテキストを通じてテキストサービスマネージャはアプリケーションに対していろいろと要求を出す。例えば、テキストを編集したい場合、以下のような手順を取る。

  • 現在入力対象となっているエディットコンテキストを取得する
  • エディットコンテキストに対してrequestEditSessionでエディットセッションを要求する
  • テキストサービスマネージャはテキストストアのrequestLockを呼ぶ
  • テキストマネージャはロックが取れたら、テキストサービスのDoEditSessionを呼ぶ
  • DoEditSessionの引数はエディットクッキーと呼ばれるもので、テキストサービスはこのクッキーを使って、IFRangeというインターフェースでテキストストアにアクセスできる。たとえば、IFRange::SetTextを呼べば、ドキュメントを書き換えることができる。
  • DoEditSessionを抜けると、テキストサービスマネージャはロックを開放する

 このように、エディットコンテキストはそれを通じてテキストストアを操作するためのものなので、かならず対応するテキストストアを1つだけ持つ。テキストストアの方は必ずしもエディットコンテキストを必要としないので、対応するエディットコンテキストを持たないこともある(と思うのだが未確認…)。

IMM32とTSFの比較

 Windows95から提供されていた文字入力の仕組みがimm32である。MSDNを見てわかる通り、IMM32の方が直感的なAPI構成をしている。テキストサービスマネージャみたいな単なる仲介だけの存在を意識しながらプログラムを書く必要がない。そもそも、大量のインターフェース群がまったくない。
 しかし、TSFではテキストストアを通じて現在編集中のドキュメントにテキストサービス側からアクセスできるが、IMM32にはそのような機能がない。(より正確には、IMR_DOCUMENTFEEDで参照できなくはないのだが、仕様が美しくないし効率的でもない。) テキストストアの導入は、TSFが複雑な構成となった理由の一つであることは確実である。テキストストア自体が複雑なものであるし、エディットセッションという初見では理解し辛い概念も、テキストストアを通じてドキュメントをロックし、テキストサービス側から操作するために生まれたものである。
 TSFは必要な機能を満たすためにこれだけ複雑になったのだろうが、複雑過ぎて理解し辛いなぁと感じた。

感想

 全体的に、やたらといろいろインターフェースがわかれており、それが概念の理解を阻害しているように感じた。例えば、ITfEditSessionはDoEditSessionと言うコールバック用のメソッドを実装するためだけに用意されたインターフェースであり、requestEditSessionに渡すためだけにこのインターフェースを実装したクラスを作らないといけないのだが、一つしかメソッドがないなら単にrequestEditSessionメソッドの引数に直接コールバック関数を渡せばいいじゃないか、という感想しか持てない。また、DoEditSessionにはエディットクッキーというのが渡され、ここからITfRange::GetTextとかのメソッドを呼んでテキストストアへとアクセスするのだが、エディットクッキーなんてなんか実装が透けて見えてそうなデータじゃなく、テキストストアに対するdelegateな感じのオブジェクトを渡してくれよ、とか思う。もっとも、このようにインターフェースを分けて層を増やしておくことで、後で仕様を拡張しやすいように余地を残しているのかもしれない。あと、私がCOMに馴染みがないから理解し辛いのであって、COMをよく知っているWindowsプログラマなら、もっと簡単に理解できるのかもしれないと思った。
 ここまで長々と解説したが、まだ他にもいろいろと知らなければ、実際のプログラムは書けない。例えば、プリエディットに下線を引きたかったら、あらかじめ線の色とかそういうのをシステムに登録して、それに対応する整数を取得する必要がある。ドキュメントのロックをIFRangeが自動で獲得するのでなく明示的にエディットセッションで獲得させるところといい、このあたりのややこしい仕様はパフォーマンスを気にしているのだろうけど、これに沿ってプログラムを書くのは結構辛そうだな、という印象を受けた。