日本語IME「ひともじ」:フォーカス移動が「ほぼ」動くようになった...。はあ、疲れた。(v0.1.5.2)
一週間のごぶさたです。
この一週間は大変でした。
「ああ、忙しくて更新できなかったのね」と思いますよね?
でも違います。まったく違うんです。
むしろこの一週間は、めっちゃ「ひともじ」の開発に時間を注ぎました。
にもかかわらず、ブログ更新しなかったのはなぜか?
まるっきり、進捗がなかったからです。
前回はこちら。
※今回は、2部構成になってます。
前半はフツーの人でも理解できるように、なにが大変だったのかを書き、後半は物好きな人向けに筆者がハマった内容を筆者なりに整理してプログラマ視点で書いてます。
フォーカス移動とは何か?
WindowsでもMacintoshでも、利用者は好きなタイミングで好きな場所をマウスなどで、クリックできます。
あたりまえですよね。
でもですね、この当たり前を実現するのって、本当に、死ぬほど大変なのです。これ、WindowsでもMacintoshでもX11でも、そうですが、利用者はどんなタイミングでクリックするかわからんわけです。ということはプログラムを作る方は、いかなるタイミングでクリックされても破綻させずに、クリック処理を受け入れないとダメなわけです。
なかでも、入力フォーカスと呼ばれる、キャレット(ここに文字入力できますよ、というのを示す細い縦棒)が動く時の処理はそれはもう、群を抜いて大変です。これをフォーカス移動といいます。
ちなみに、Windowsでは昔(1990年ころ)からアプリ開発で必ずハマるといっていいポイントが2つあります。一つが、フォーカス移動(WM_FOCUS)、もう一つは描画処理(WM_PAINT)。この2つを聞いて苦い思い出が浮かぶプログラマは多いはずです。
何が難しいかというと、タイミングやマシンの速度によって動いたり動かなかったりするんですね。しかも、利用者の操作なんて、無限の組み合わせがあります。なので、どの組み合わせでもハタンせずに動くように作る、パターンを網羅するってのがめちゃくちゃ難しいんです。
さらにTSFがからむと地獄
さらに「ひともじ」の場合、かな漢字変換をサポートしてくれるTSFというフレームワーク(テキスト=サービス=フレームワーク)の上で動くように作らないといけないのですが、このTSFが恐ろしくクセのきつい、扱いづらいフレームワークなんです。
というのは、TSFを使って自力でIME作ろうなんてやつは、おそらく存在しないんですね。だってIMEを必要とするのは東アジア圏くらいで、英語圏では知らん開発者ばかり。IMEを必要とする日本でだって、「日本語IMEの自作」なんて聞いたことないでしょ?
ようするに取り組んでる人が極端に少ないわけ。
なので、ブログとかも含めてTSFに関する情報ってホンマに少ない。前にも書いたけど、chatGPTでもGeminiでも、情報が少なすぎて、踏み外しや誤誘導がめちゃくちゃ多くて、生成AIのいうこと信じて実装してもさっぱり動かんなんてのもしょっちゅう。(特にchatGPTはコード生成に弱い)
未対応だと落ちる
正直言って、ホントはフォーカス移動なんて対応したくない。だけど、対応せざるをえないんです。
てのは、やらんと落ちる(異常終了する)から。
なぜ落ちるかというと、フォーカスが移動すると、入力中(波線とか下線が引かれた状態)の文字の状態が変わる(通常は確定される)。でも、その確定した事実をプログラム側が積極的に取りにいかないと、教えてもらえない。教えてもらえないと、IMEはまだ入力中だと思って処理をする。でも実際には入力中の文字なんてないから、矛盾が生じる。だから落ちる、というリクツ。
結局、落ちないようにするには、現在の状態を事細かに教えてもらいながら、利用者が何をしようとしているかを考えながら、制御してやらないといけない。かなりの綱渡りをしないとダメってことです。
しかも、IMEはDLLといういわば寄生型プログラムなので、そのIMEを使ってるアプリごと落ちるから、作ってる途中の文書とかが消えてなくなる。
これは、あまりに迷惑な話でしょ?だから、落ちるのを放置するわけにもきかず、いやいや対処したってのが実際のところ。
この面倒くさいことを、試行錯誤しながら、1週間以上やってたわけですよ。「疲れた」ってのもわかってもらえる?
で、具体的にどんなことをしなきゃいけないか、ってのは以下で書きます。が、かなりプログラマ向けの内容なので、一般の方はスルー推奨です。
プログラマのための覚書
本気でIMEなんて面倒なものを作りたいなんてモノズキはそうそういないと思うけど、せっかくいろいろ調べた、いや、正確には生成AIに調べてもらった、だな、ので、それをまとめておきたいと思う。
ここでは、特にフォーカス移動にからめて、ITfCompositionの生存管理に関する話を書いておく。もっとも、筆者もTSFについてはほぼシロートなので、ウソを書いてる可能性も高いので、丸呑みは危険だ、ってことを最初に書いておく。
COM
これはもう前提でいいと思うんだけど、TSFは全面的にCOMを採用している。なので、COMのお作法(QueryInterface、AddRef、Release)あたりは知ってるものとして省略する。COMについては、AIに聞けば、かなり丁寧に教えてくれるが、そもそもがC++のクラスの概念とCOMのクラスの概念がチャンポンになると、めちゃくちゃ混乱してくるので、COMはキチンと理解しておく方がいい。
ITfComposition
変換中文字列、変換中の状態管理は、このITfCompositionに集約されている。これももちろん、COMオブジェクト。
文字入力を始めると、TSFがITfCompositionのオブジェクトを生成し、確定する(EndCompositionを実行する)と、オブジェクトが破棄される。ところが、これがいろんな理由でIME側の知らない間になくなったりする。
なので、これの生存確認が、フォーカス移動で落ちないようにするためにかなり重要になる。
ITfContext
これはCompositionを保有するCOMオブジェクト。基本的には1つのウィンドウが1つのITfContextを保有するものらしい。多くの場合、ウィンドウごと(アプリによってはペインごと)にContextが存在するっぽい。TSFの多くはContextを頼りにして、情報を集めることになる。
ITfThreadFocusSink
これはOnSetThreadFocusとOnKillThreadFocusのコールバック(Advise)のを提供するCOMインタフェース。後述のOnEndEditを呼んでもらうには、OnSetThreadFocusでITfTextEditSinkのAdviseSink登録をしないといけない。
ITfTextEditSink
これは、Compositionに対する変更が加わった時に呼ばれるコールバック関数(OnEndEdit)持つCOMインタフェース。これを実装することで、Compositionへの変更を通知してもらえる。
が、最大の問題は、このOnEndEditを読んでくれる保証がないこと。
信じがたい話だが、コールバック登録(AdviseSink)をしても、OnEndEditが必ず呼んでもらえるという保証はないらしい。実際、ひともじでも一切呼んでもらえない時もあるし、毎回呼んでくれる時もあった。かなり気まぐれな印象。
なので、OnEndEditの実装はいるんだけど、これが呼ばれなくても落ちないようなCompositionの生存管理が必要になる。
ITfCompositionView
ホントにCompositionが生きてるか(有効か)どうかを調べるために必要なCOMオブジェクト。このオブジェクト自体はITfRangeの取得しかできないが、これがあれば、対となるITfCompostionは「多くの場合」存在する。これもまた、いやらしい話だが、ITfCompositonViewがあるからといって、ITfCompositiionが有効とは限らないし、ITfCompositionViewからITfCompositionを取得することもできない。
でも、「経験的に」、ITfCompositionViewがあるときは、StartCompositionで取得したITfCompositonは使えることが「多い」。
フォーカス移動で落ちない戦略
ひともじの場合、Activateで、ITfSetTreadFocusSInkのAdvise登録を行い、OnSetThreadFocusでITfTextEditのAdvise登録を行っている。
内部では入力開始時(DoEditSessionでStartCompositionした時)に、ITfCOmpostionと、ITfContextを保存している。
OnEndEditがくると、保持している上記の2つの値との比較を行い、違っていれば、「ああ、フォーカス移動によって新たなCompositionが作られたな」と判断し、現在のCompositionをReleaseしている。
ただ、上述の通り、OnEndEditが来ないこともあるので、その場合に備えて、OnKeyDownでEditSessionを作る際にもCompositionの生存確認処理を行っている。Contextが同じで、CompositionViewが取得できれば、保管しているCompositionは継続利用できるものとし、取得できなければ、CompositionをReleaseし、新たにStartCompositionを実行するロジックとしている。
この説明だけを見てもワケがわからないと思うので、下記のサイトからソースをチェックアウトして、見ていただくことを強く推奨。
現状、この構造で(バグに近い仕様はあるが)、以下のパターンについて落ちない様子である。
A) 同じ編集フィールドの中でマウスクリック
B) 同じContextの違う編集フィールドをマウスクリック
(OnEndEditを通らない場合、最初の1文字が無視されてしまう)
C) 同じアプリの違うコンテキストに属する編集フィールドをマウスクリック
D)異なるアプリをクリック
ソース提供
現状のソースは以下で公開している。この記事はバージョン0.1.5.2の内容をもとに書いています。
最後に
現状で、フォーカス移動を考慮した(落ちない)プログラミングと動作検証と、一通り終わりました。
まだ、Windowsアプリとして守らないといけないお作法がいくつか残っています。(タスクバーやフローティングバーへのアイコン表示、32bitアプリと64bitアプリの両方で動作させるためのインストーラなど)
このあたりの作業が終わった時点で、v0.1.7として、バイナリパッケージの公開をしようかと予定しています。
意外にやるなー、ヒマなヤツやなー、と思った方はぜひ、スキ、フォローをお願いします。
この開発ストーリーの詳細は、以下のマガジンをごらんください。



コメント