2023年11月5日日曜日

Text Services Framework (TSF)

フルスクリーンでゲームを作る場合、IMEを制御して自動で表示されるものをすべて止めて、自前で描画する必要がある。

かなり昔に作ったことがあり、今回そのソースを移植して対応してみた。

一応動くようになったんだけど、その頃と違い最近は予測変換が表示されるようになっている。
このリストがうまく取得できない。
色々調べてみると、IME32は一度廃止された?されそうになった?がまた復活したという経緯があるらしい。
(個人のブログでIME32がなくなるから、今後は使えないというような記事を見つけた)

では新しいAPIは何になるのかというと、TSFというものらしい。
もしかするとこっちのAPIを使えば予測変換でも正しくイベントを受けられるかもと思い対応してみることにした。


TSF


Vistaの頃にできたものらしく、そこまで新しくはないAPIでCOMベースで作られているためかなり使うのにハードルが高い。
まったく知らなかった。

サンプルプログラムをダウンロードして実行してみるもいまいちよくわからない。
どうやら、TIP(Text Input Processor)を使う側と作る側両方のソースが含まれているようだ。
まずは個人で最低限のサンプルを載せてくれている人のソースをいくつか参考にしながら調査すると、ほんとの最小限は「ITextStoreACP」と「ITfContextOwnerCompositionSink」だけを実装すれば動くっぽい。

サンプルほぼ丸写しで実装してみた。
TextStoreはその名前の通り、EditBoxの持ってるテキストを管理するクラスという認識。
IME32の場合は、変換中の文字列だけが管理され変換後は別途管理が必要だったので一元管理できる分こっちのほうがいいかも。前後の文字列から変換結果を変えたり、変換済みの文字列を再変換できる機能があるからだと思われる。

内部で持つものは、「ITfDocumentMgr」「ITfContext」「ITfProperty」「ITfCategoryMgr」「ITfDisplayAttributeMgr」「ITextStoreACPSink

これらは内部で保持せず、使うたびに取り出す実装になっているのも見たがどちらがいいのかは判断つかず。MS-IMEとGoogle日本語入力を切り替えてもそのまま使えているので、切り替えタイミングをイベントで受けて、そのたびに作り直しということもないので保持しておくことにした。

最後のTextStoreACPSinkというのがイベントを通知させるためのもので、これをAdviseSinkした人にイベントが通知される。

ここでややこしいのが、TextStoreACPSinkを受け取るのがTIPの本体の方で自分ではない。
MS-IMEやGoogle日本語入力がAdviseSinkをしに来るから、それらに対してこちらからイベントを通知してあげる側になる。


変換文字列


先ほどのTextStoreACPSinkとは逆で、今度はこちらがイベントを受けることにより変換文字列の変更通知を受け取れるようにする。
ITfContextOwnerCompositionSink」を継承して、必要なイベント関数を実装する。
***SinkはAdviseSinkすると通知されるようになるはずなんだけど、これの為のAdviseSinkは見当たらない。CreateContextにITextStoreACP*として、自分自身を指定しているのでこれが代わりになっているのかも?

ここまでできるとImmGetCompositionStringの代わりができたことになる。

Property->EnumRangesで「IEnumTfRanges」を取得
EnumRanges->Nextで「ITfRange」を取得
Property->GetValueにRangeを指定して、「TfGuidAtom」を取得
CategoryMgr->GetGUIDにGuidAtomを指定して、「GUID」を取得
DisplayAttributeMgr->GetDisplayAttributeInfoにGUIDを指定して、「ITfDisplayAttributeInfo」を取得
DisplayAttributeInfo->GetAttributeInfoで「TF_DISPLAYATTRIBUTE」を取得
Range->QueryInterfaceで「ITfRangeACP」を取得
RangeACP->GetExtentで、変換中の区切り位置を取得
Enum分ループすると、それぞれのTF_DISPLAYATTRIBUTE.bAttrと、区切り位置で、変換中の文字列の区切り位置と属性が得られる。


変換候補


次に変換候補のリストを取得してみる。
メンバに「ITfUIElementMgr」を追加する。
イベントを受け取るためのインターフェース「ITfUIElementSink」を継承して、必要なイベント関数を実装する。
ITfThreadMgrEx」からQueryInterfaceで「ITfSource」を取り出し、Source->AdviseSinkで自分自身を登録する。
BeginUIElementを受けた際、一緒に渡されるbShowにFALSEを返却すると、変換候補のWindowが表示されなくなる。
また、ルール通りにTIP側が実装されていれば、FALSEを返した場合即座にUpdateUIElementが呼ばれるらしい。なので、リスト取得の実装はUpdateの方にのみしておけばいいみたい。

このイベントが受けられるようになるとImmGetCandidateListの代わりができたことになる。

UpdateUIElementに渡されたElementIDを使って、UIElementMgr->GetUIElementで、「ITfUIElement」を取得
UIElement->QueryInterfaceで「ITfCandidateListUIElementBehavior」を取得

※未解決
Google日本語入力だと、予測変換のタイミングでCandidateListの取得は失敗する。
Tabや↓矢印キー押下後は普通に取得できる。
MS-IMEでは予測変換のタイミングで取得できる。
ただ、MS-IMEでは予測変換時でも先頭の項目が選択された状態になっているため、↓矢印を押下すると2番目の項目が選択されてしまい、どちらも微妙。
変換候補を画面表示した状態であれば未選択状態になっており、↓矢印選択時に1番目が選択状態になる。これと同じ挙動にしたいが、やり方がわからず。

CandidateListUIElement->GetSelectionでリストの選択位置を取得
CandidateListUIElement->GetUpdatedFlagsでリストの前回からの変更点を取得
CandidateListUIElement->GetCurrentPageでリストの現在のページ番号を取得
CandidateListUIElement->GetCountでリストの項目数を取得
CandidateListUIElement->GetStringでリストの指定インデックスの文字列を取得

今のところ、UpdatedFlagsで取得した値が「TF_CLUIE_COUNT | TF_CLUIE_STRING」の時に、リストをすべて取り直すようにしている。
GetPageIndexを使えば、現在のページのみに絞れるためこっちを使った方がいいかも。


変換モード


通常タスクバーに埋まってる「A」とか「あ」とかの状態を取得する方法

変換モード

これがなかなか見つからなくて1週間くらい探してやっと見つけた。

メンバに「ITfCompartmentMgr」を追加する。
イベントを受け取るためのインターフェース「ITfCompartmentEventSink」を継承して、必要なイベント関数を実装する。

登録は、CompartmentMgr->GetCompartmentで、「ITfCompartment」を取得
Compartment->QueryInterfaceで、「ITfSource」を取得
Source->AdviseSinkで自分自身を登録するという流れ。

ただ2回行う必要があって、1つ目のGetCompartmentはGUIDにGUID_COMPARTMENT_KEYBOARD_OPENCLOSEを指定する。2つ目は、GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSIONを指定する。
1つ目はIMEのOpenとCloseのタイミングを得るためで、ImmGetOpenStatusの代わりになるもの。
2つ目は、ImmGetConversionStatusの代わりになるもの。

ちょっと癖があって、Close状態でも変換モードは直前のものが取れてしまうため、自分で「A」にする必要がある。Open状態はInputModeの値をそのまま使えばいい。
取得できるEnum値はTF_CONVERSIONMODE_*で、IME_CMODE_*と同じ値らしい。


これでImm系の関数を一切使わず、TSFに移行できた。

当初の目的の予測変換のタイミングについては、MS-IMEについては完ぺきに取得できるようになったが、候補リストの先頭が選択されている状態になっている問題は残っている。
Google日本語入力に関しては、予測変換が表示されるタイミングでOnUpdateUIElementは呼び出されはするが、候補リストを取得できない問題が残っている。



1/22追記


変換候補については、自前で描画せずに任せることにした。変換中は別Windowsが表示されることになるが大した問題ではないと思うことにした。
変換候補に実際は描画しない文字列もたくさん含まれているため、描画用のテクスチャがどんどん消費されていくのも防げる。

ItextStoreAcp::GetTextExtの引数に渡されるRECT型の変数に、変換中の矩形をスクリーン座標で返すと変換候補の画面を出す位置を調整してくれるようになる。
似たような関数でItextStoreAcp::GetScreenExtがあるが、面倒なので同じ座標を返すようにしている。今のところ不都合は発生していない。



0 件のコメント:

コメントを投稿