投稿日 : 2015/10/02 3:59:48
TSFで日本語変換

Windows8以降で自前で日本語入力するには

 一般的なWindowsプログラムでは、エディットボックスを作るだけで日本語入力が行えますが、
 Direct3Dなどを使ってゲームを作り、これにチャット機能のような日本語入力を実装しようと思っても、
 Direct3D上ではエディットボックスのようなGUIが使えないため、そのままでは実現出来ません。

 このためDirect3D上で日本語入力を行うには、入力した文字の表示や変換リストなどの管理を
 自前で行う必要がありますが、これはWindows7まではInput Method Manager(IMM)という
 APIを利用して実装するのが基本でした。
 ※ちなみに日本語入力を行うソフト自体のことをInput Method Editor(IME)というので、
  ごっちゃにしないように気をつけてください(例えばMicrosoft IMEとかATOKなど)

 しかし、このIMMはWindows8以降サポートされなくなってしまったため、
 Windows8以降で日本語入力を行うにはText Services Framework(TSF)という
 APIを使う必要があります。
 実際にはAPIというよりは単にインターフェースを実装したクラスを作ってシステムに登録しておけば、
 必要な時にそのメソッドが呼び出されるというコールバック的な感じになっています。

 またここが大事ですが、IMM自体のAPIが完全に別物になったというわけではなく、
 下にも書いていますが、実際には変換リストの取得方法が異なるだけで、
 入力関連のメッセージは今までのIMMと同じくWM_IME_****として通知されたり、
 そこで候補などを取得するにもやはりImmから始まる関数を使用
することになります。

 ちなみに、Windows7の時点で既にIMMはTSFのラッパーで動いていたようで、
 未実装の機能があったりしていろいろと不具合などがあります。
 ※OS標準のIMEではうまく変換出来るがOffice付属のIMEだとクラッシュするなど

 実はTSF自体はWindows2000以降で使用することが出来るのですが、
 マイクロソフトは以前からIMMを廃止するのでこれからはTSFを使用しろと言っているにも関わらず、
 分かりやすい資料がほとんど無く、マイクロソフトのTSF解説ページではAPIの説明に行くとほぼ英語のままで、
 未だに理解しにくい状態となっています。
 (これはIMEを作る側も考慮した資料となっており、知りたい情報にたどり着くことが困難で結局読むのを諦めました)

 さらにネットで調べたところ、Direct3DのSDKやWindowsSDKにTSFのサンプルがあることが分かったのですが、
 これもまた解析しづらいコードになっており、しかもDXUTを使っているためその知識も必要だったりして、
 とにかく最初はまったく意味が分かりませんでした。

 また、他のサイトではあるインターフェースを実装すれば変換候補が取れるという情報があったのですが、
 実際にやってみるとそれはあくまでも指定した文字列に対する変換候補のリストが取れるだけで、
 通常の日本語入力のように打ち込んだ文字を表示したり、
 文節ごとに区切って変換を行うといった本来の日本語変換の使い方とはまったく違うものでした。
 (つまり通常の日本語入力処理ではなく、変換辞書を直接制御するみたいな感じ)

 その点IMMに関しては大量に資料があるため、ソースをコピペすれば結構簡単に実装出来るということで、
 ほとんどの開発者はずっとIMMを利用していましたが、Windows8になって完全にIMMが切られてしまったため、
 それまで動いていたアプリが動かなくなってしまい、いたるところで問題になっているようです。

 ちなみに以前会社で作ったあるアプリについて、そろそろWindows7の端末の在庫が無くなるので、
 今後はWindows8以降のOSに置き換えるという話になったのですが、
 当時TSFがよく分からないということでIMMにて実装していたため、
 そのアプリを試しにWindows8で実行したところやはりクラッシュしてしまいました。

 そのため今回TSFへ移行する必要が発生してしまったため、仕方なく調べることになったのですが、
 結局はサンプルソースを自分で解析して理解しなければならないという事を、これから同じようにTSFを
 学ぶ人が繰り返さなければならないといった無駄を省くため、そして未だにまともな情報が無いことも含め、
 その時に調べて理解した事をここで情報として公開することにしました。

 なお、以下はひとまず要点をまとめただけなので、IMMを多少知っていたほうが理解しやすいかと思われます。
 (メモをさくっと貼り付けて手直ししただけのため、多少見づらいかもしれせん)

IMMとTSFの違い

 ・基本的に変換候補の取得方法が異なるだけ
  ※IMM非対応のOSではIMM関数から変換候補が取得出来ないため、
   変換候補が取れることを想定しているとNULLで落ちたり、
   IMMに対応していたとしても、変換候補の中にさらに他の変換候補を出すような
   日本語エンジンを使っている場合に対応が面倒になる。
 ・TSFの対応OSはWin2000からのため現状特に問題は無い

TSFで変換候補を取得するには

 ・ITfUIElementSinkを実装した独自クラスを作成し、スレッドマネージャ(ITfThreadMgr)の
  ソース(ITfSource)に登録(AdviseSink)しておくことで、変換中に該当する仮想関数が
  呼び出されるのようになるので、ここで候補リストの生成が行える。
 ・一度に全ての候補文字列が取得可能
 ・現在選択中の候補の文字列のインデックスが取得出来るので、
  これを利用してカーソルなどの表示が可能

実装方法

 ①入力中の全ての文字列を管理するためのテキストバッファ(WCHAR配列など)を用意し、
  これに対する現在のカーソル、選択中の範囲を処理するための変数を用意する。
  また、必要ならば挿入モードか上書きモードかを管理する変数も用意する。
  ※要はエディットボックスの機能を自前で処理するもので、
   IMEによる変換中の文字列や変換候補リストとは関係無いプレーンなバッファ

 ②IMEがOFFの状態では入力した文字そのもの(aや1など)やシステムキー(DELやBSなど)などが
  WM_CHARに送られるので、これを確認してカーソル位置や選択範囲を変更したり、
  文字の場合は現在のカーソル位置に文字を挿入したり上書きする。
  また、選択中の文字列があった場合は先にこれを削除したりなどの処理を行う。
  (文字入力や削除などが行われた場合はそれに合わせてカーソルを移動したり、
   また例えば選択範囲が存在していた場合で文字を追加する場合は、
   先に選択範囲を削除してから挿入するなど、文字列に対する基本的な処理も行う)

 ④IMEがONの場合はWM_CHARは来なくなり、代わりにWM_IME_****イベントが通知されるので、
  IMEの開始、変換、完了などのイベントを監視して、入力中の文字列や1文字ごとの属性の取得、
  変換リストの生成などを行い、それらを表示するモードに移行する。
IME関連
ImmGetCompositionString(,GCS_COMPSTR,,)入力中の文字列を取得(WCHAR配列)
ImmGetCompositionString(,GCS_COMPATTR,,)入力中の文字列の1文字ごとの属性(BYTE配列)
ImmGetCompositionString(,GCS_CURSORPOS,,)入力中の文字列のカーソル位置(文字単位)
ITfUIElementSink関連
BeginUIElement() 変換候補リストが生成されると呼び出されるので、
自前で候補リストを管理するためにpbShowにFALSEを入れて返す。
  ※TRUEにするとシステムUIが表示され、その後UpdateUIElementが来なくなる(EndUIElementは来る)。
 変換候補リストは変換対象の文字列が変わらない限り変更は無いので、
 ここで全ての候補リストを保存しておくことも出来る。
UpdateUIElement() 変換候補が変更されると呼び出される。
BeginUIElement()イベントからリスト自体は変わっていないため、ここではカーソルの位置だけを更新する。
例えば一度に表示する数を制限するような実装を行う場合は、カーソル位置に合わせてここでリストを更新するとよい。
EndUIElement() 文字を決定したりキャンセルした場合や、変換対象の文字列が変更(「あい」→「あ」など)された場合に
呼び出されるので、ここで保存していた候補リストをクリアする。

  変換中に対象の文字列を変更しその文字に対して変換を行うと、
  一度EndUIElement()が呼び出されてから新たにBeginUIElement()が呼び出されるため、
  基本的にはBegin~End間で今回変換対象の文字列(文節単位)に対する変換リストだけを管理すればよい。

 ⑤WM_IME_COMPOSITIONのGCS_RESULTSTRフラグを確認したら、
  候補リストなどをクリアしてIME入力モードを終わらせる。
  また、このメッセージをそのままDefWindowProcに渡すことで、システムが自動的に1文字ずつWM_CHARを送るため、
  WM_CHARに渡された文字をそのままテキストバッファに登録していけば、最終的なテキストバッファが完成する。

  なお、変換を確定せずに次の文字を入力した場合は、GCS_RESULTSTRと同時にGCS_COMPSTRフラグも含まれるため、
  GCS_RESULTSTRを処理したら即座にリターンせずに必ずGCS_COMPSTRもチェックする。
  ※DefWindowProcの戻り値は基本0なので、戻り値はあまり気にしなくて良い?

キーボード以外でIMEを操作

 IMEのON/OFF切替
  ①ImmGetContext()でIMEコンテキストを取得
  ②ImmSetOpenStatus()の2番目の引数にONの場合はTRUE、OFFの場合はFALSEをセットして呼び出す
  ③ImmReleaseContext()でIMEコンテキストを開放

 漢字モードのON/OFF切替
キーコード「VK_KANJI(0x19)」をキーボードイベントとして押下したことにする。
keybd_event( VK_KANJI,0,0,0 );                  // DOWN
keybd_event( VK_KANJI,0,KEYEVENTF_KEYUP,0 );    // UP

 かなモードでの入力(日本語キーボード上でひらがなキーを押したものがそのまま入るモード)
  かな入力モードかどうかはシステムが管理しているため、まずGetKeyState(VK_KANA)にて押下状態を取得し、
  押されていなければkeybd_event()にて押したことにし、そのあとで通常の英語キーを押したことにする。
// かなモードでなければかなモードに変更
if( !GetKeyState(VK_KANA) ) {
    keybd_event( VK_KANA,0,0,0 );
    keybd_event( VK_KANA,0,KEYEVENTF_KEYUP,0 );
}
// 「あ」はキーボード上の「3」キーであり、そのキーコードは「0x33」となる
keybd_event( 0x33,0,0,0 );
keybd_event( 0x33,0,KEYEVENTF_KEYUP,0 );
// これ以降もかなモードが継続しているため、必要ならばもう一度VK_KANAを押下してローマ字に戻す

 シフトキーを押しながらの入力
  大文字の英字を入力したい場合は、シフトキーが押されている状態をエミュレートする。
  なおシフトキーは拡張キー扱いのため、キーボードイベントに拡張キーであることを示す
  フラグを付けて呼び出す必要がある。
  ※正確にはシフトキー自体は拡張キーではなく、次に押すキーが拡張扱いとなり、
   押されたキーによりシステム側で適切な文字コードに変更されWM_CHARに通知される
   (例えば通常の英語文字なら大文字に)
// 大文字の「A」を入力する場合は0x41の押下に拡張フラグを設定する('a'の文字コードは0x41)
keybd_event( VK_SHIFT,0,0,0 );
keybd_event( 0x41,0,KEYEVENTF_EXTENDEDKEY,0 );
keybd_event( 0x41,0,KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP,0 );
keybd_event( VK_SHIFT,0,KEYEVENTF_KEYUP,0 );
 コントロールキーを押しながらの入力
  コピー&ペーストのようにコントロールキーを押しながら行う操作は、
  拡張キー扱いではなくシステムが完全に別の処理と認識するため、
  この場合は単純にコントロールキーを押した状態にしてから通常のキーを押したことにすればよい。
// CTRL+Cの場合('c'の文字コードは0x43)
keybd_event( VK_CONTROL,0,0,0 );
keybd_event( 0x43,0,0,0 );
keybd_event( 0x43,0,KEYEVENTF_KEYUP,0 );
keybd_event( VK_CONTROL,0,KEYEVENTF_KEYUP,0 );
  WM_CHARのwParamに対応するキーコードが来るので、テキストバッファなどを適切に処理する。
   VK_BACK バックスペース
   0x01 CTRL+A
   0x03 CTRL+C
   0x06 CTRL+F
   0x0E CTRL+N
   0x16 CTRL+V
   0x18 CTRL+X
   0x1A CTRL+Z
    …など
     (詳細はCustomUIサンプルソースから「Junk characters we don't want in the string」で検索)

その他イベント

 IMEのON/OFFイベント
  IMEがONかOFFになったかはWM_IME_NOTIFYのwParam==IMN_SETOPENSTATUSを監視する。
  現在の状態がONかOFFかはImmGetOpenStatus()にて取得する。

 入力モード変更イベント
  モードが切り替わったかはWM_IME_NOTIFYのwParam==IMN_SETCONVERSIONMODEを監視する。

  この時ImmGetConversionStatus()を呼び出すことで、切り替わったあとのモードを取得出来る。
  なお、現在のモードはImmGetConversionStatus()の2番目の引数に返るが、
  実際の変換状態はこれらのフラグの組み合わせになる。
IME_CMODE_JAPANESE 日本語入力状態か(IME_CMODE_NATIVEと同じ)
IME_CMODE_FULLSHAPE 全角モードか(フラグが無ければ半角モード)
IME_CMODE_KATAKANA カタカナ入力状態か
IME_CMODE_ROMAN ローマ字変換モードか

  入力状態を把握するには、以下のフラグの組み合わせを「&」した値がそのフラグと同一かで判断する。
  ※言語はひとまず日本語のみ
半角英数入力
(IME_CMODE_ROMAN                                                                )
全角英数入力
(IME_CMODE_ROMAN | IME_CMODE_FULLSHAPE                                          )
日本語ローマ字入力
(                  IME_CMODE_FULLSHAPE | IME_CMODE_JAPANESE                     )
日本語半角カタカナ入力
(                                        IME_CMODE_JAPANESE | IME_CMODE_KATAKANA)
日本語全角カタカナ入力
(                  IME_CMODE_FULLSHAPE | IME_CMODE_JAPANESE | IME_CMODE_KATAKANA)
※ローマ字とカタカナはIME_CMODE_ROMANがある時と無い時があるため、IME_CMODE_ROMANは無視すればよい

  ローマ字ではなくかな入力状態(キーボードの平仮名文字を直接入力)かは、GetKeyState(VK_KANA)にてかなキーが押されているかで判断出来る。

描画方法

 以下、順番にオーバーラップで描画する。

 ①始めに自前のテキストバッファの内容を1文字ずつ表示する。
  この時、選択範囲が指定されている場合は反転などの装飾を行うなどで、
  選択中の範囲が分かるようにする。
  また、表示位置やクリック座標などを計算するため、1文字ごとにサイズ情報も記録しておく。
  ※フォントが等幅では無い場合があるため

 ②編集中の位置が分かるように、テキストバッファ上での文字の位置にカーソルを表示する。
  挿入モード時は「|(縦棒)」を点滅したり、上書きモード時はカーソルにある文字を反転するなどして、
  どちらのモードであるか分かるようにする。

 ③IME入力時は入力中の文字列を表示する必要がある。
  まずWM_IME_COMPOSITIONのGCS_COMPSTRフラグを監視して、
  入力中の文字列とその文字1文字ずつの属性を取得する。
  次にテキストバッファのカーソルを原点とし、この文字列を属性に合わせて装飾して表示する。
属性 内容
ATTR_TARGET_CONVERTED(0x01) 選択されていて変換されている文字(変換中の文節)
ATTR_TARGET_NOTCONVERTED(0x03) 選択されていて変換されていない文字(変換してない文節)
ATTR_INPUT(0x00)
ATTR_CONVERTED(0x02)
ATTR_INPUT_ERROR(0x04)
ATTR_FIXEDCONVERTED(0x05)
未変換状態や変換後でフォーカスが無い状態のため、特に装飾せずに表示(必要ならば自由に装飾可)
例)「あいうえお」と入力して変換をした場合
  IMEにより「あい」「うえ」「お」と分解され、「藍」「飢え」「於」と変換されている状態で、
  現在変換中の文字列が「うえ」だった場合に各関数で取得出来る情報は以下の通り。
   ImmGetCompositionString(,GCS_COMPSTR,,) → 「藍飢え於」の文字列
   ImmGetCompositionString(,GCS_COMPATTR,,) → 「0x02,0x01,0x01,0x02」取得した文字列の属性

 ④変換中の文字列にカーソルを表示する。
  カーソル位置はImmGetCompositionString(,GCS_CURSORPOS,,)で取得出来る
   例)上記「藍飢え於」と変換されていた場合で、文節ごとのカーソル位置はそれぞれ以下の通り。
      ImmGetCompositionString(,GCS_CURSORPOS,,)
       「藍」変換時 = 0
       「飢え」変換時 = 1
       「於」変換時 = 3
      ※つまり区切りごとの配列番号

 ⑤最後に変換候補リストがあれば表示する。
  リストは必ず入力中の文字列の近くに出す必要は無く、自由に選択処理を実装出来る。
  なお、現在選択されている候補文字列には色を変えるなどの装飾を行う。
  ※変換候補は常に何らかの文字列が選択されている
   (変換中に一度候補が消え再び候補を出したときに、以前のカーソルが維持されている必要があるため、
    初期位置はBegin内で取得しておく)
  ※現在選択中の候補のインデックスは、UpdateUIElement()イベント時に要素マネージャから
   ITfCandidateListUIElementを取り出し、その中のGetSelection()を呼び出すことで取得出来る
コメントはまだ登録されていません。
コメントする
名前
コメント
※タグは使用出来ません
この記事に関連するタグ
TSF(IME)