X11環境でIMEの仮入力の情報を取得する

LinuxのX11向けのGUIアプリケーションでテキスト入力を扱いたい場合、IMEの存在を意識しなければならない(特に日本人ならば)。
IMEなしならば、キーボードイベントを見て、そのキーのアルファベットをプリントするだけで済むが、IMEが存在する場合、IMEから送られてくる文字列をきちんと処理する必要がある。
そのためには、X11のIMEフレームワークであるXIMの仕様を知る必要がある。

これらの仕様について解説し、実際のコードも紹介している記事はすでにある。

上記の記事を参考にしたと思われる日本語の記事もある。

しかし、実はこれらの記事ではIME内で確定した文字列の情報しか取得しておらず、IME内でまだ確定していない仮入力状態の文字列(例えば変換前の元のひらがな)は取得できていない。
仮入力の文字列を取得できていないと、それらの文字列は当然描画できないし、例えば変換前の文字列を使った入力補完なんかも実装できない。
ある程度ちゃんとしたX11アプリケーションではこれらがちゃんとできていることからも想像がつく通り、XIMではこれらの状態を渡すインターフェースがある。

先述のブログのコードのうち、IMとの通信のためのコンテキスト構造体であるXICをつくっているところを見てみよう。

1
2
3
4
5
XIC ic = XCreateIC(xim,
/* the following are in attr, val format, terminated by NULL */
XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
XNClientWindow, win,
NULL);

XNInputStyleの値としてXIMPreeditNothing | XIMStatusNothingを指定している。このXIMPreeditNothingというところが仮入力状態をどう扱いたいかをしてするものである。
XIMPreeditNothingは仮入力状態を渡すことなく動いてしまう。そのため、代わりにXIMPreeditCallbackを指定しなければならない。
このオプションを使う場合、クライアント側に仮入力の状態が変化した際のコールバックをXNPreeditAttributesとして渡してあげる必要がある。
コールバックにはXNPreeditStartCallbackXNPreeditDoneCallbackXNPreeditDrawCallbackXNPreeditCaretCallbackの4つがある。

  • XNPreeditStartCallback: 仮入力がスタートしたときに呼ばれる
  • XNPreeditDoneCallback: 仮入力が終了したときに呼ばれる
  • XNPreeditDrawCallback: 仮入力状態の文字が更新されたときに呼ばれる。呼び出されたときに変化した文字列の情報が渡されてくる。
  • XNPreeditCaretCallback: 入力カーソルの位置が変わったときに呼ばれる。呼び出されたときに入力カーソルの位置の変化情報が渡されてくる。

これらをXVaNestedList型としてXNPreeditAttributesとしてXCreateICに渡す必要がある。各コールバックはXIMCallback構造体へのポインタとして定義される必要がある。
XIMCallback構造体の定義は以下の通りである。

1
2
3
4
typedef struct {
XPointer client_data;
XIMProc callback;
} XIMCallback;

XPointerはchar *、つまり汎用のポインタで、XIMProcの定義は

1
2
3
4
5
typedef void (*XIMProc)(
XIM,
XPointer,
XPointer
);

となっている。第一引数はXIM型となっているが、こちらのドキュメントではXIC型となっている。どちらが正しいかわからないが、今回はこれは使わないのでスルー。
第二引数はXIMCallbackclient_dataが渡されてきて、第三引数にはコールバックの種類によって必要な情報がサーバー側から送られてくる(以後、call_dataと呼ぶ)。XPointer型を適宜キャストして使うことになる。

XNPreeditStartCallbackではcall_dataにはNULLが渡されてくる。XIMProc型の関数の返り値はvoidとなっているが、実際はこのコールバックは仮入力文字列の長さの上限を返さなければならない。-1とした場合、上限はない。
今回はこのように定義する。

1
2
3
4
5
6
7
8
9
static int preedit_start_callback(
XIM xim,
XPointer client_data,
XPointer call_data)
{
printf("preedit start\n");
// no length limit
return -1;
}

XNPreeditDoneCallbackcall_dataNULLが渡されてくる。今回は仮入力中の文字列を出力したいだけなので、特に何もしないでいいだろう。

1
2
3
4
5
6
7
static void preedit_done_callback(
XIM xim,
XPointer client_data,
XPointer call_data)
{
printf("preedit done\n");
}

XNPreeditDrawCallbackが仮入力文字列に変化が起きた場合の処理である。call_dataXIMPreeditDrawCallbackStruct構造体として渡されてくる。

1
2
3
4
5
6
typedef struct _XIMPreeditDrawCallbackStruct {
int caret; /* Cursor offset within pre-edit string */
int chg_first; /* Starting change position */
int chg_length; /* Length of the change in character count */
XIMText *text;
} XIMPreeditDrawCallbackStruct;

仮入力中のすべての文字が渡されてくるのではなく、変化した文字列の情報が渡されてくる。
chg_firstからchg_length文字数分がtextに変化したという感じである。
ただし、筆者の環境のIMEはchg_firstに常に0が渡されて、textに仮入力中の文字全部が渡されるような動作をした。
しかし、仕様の上では一部しか渡されてこない可能性もあるので、一応注意して実装する。
なお、文字数のカウントはバイト数ではなく、システムの文字コード(通常はUTF-8のはず)で解釈した場合の文字の数であることに注意。

XIMText型として文字列の情報が渡されてくる

1
2
3
4
5
6
7
8
9
typedef struct _XIMText {
unsigned short length;
XIMFeedback *feedback;
Bool encoding_is_wchar;
union {
char *multi_byte;
wchar_t *wide_char;
} string;
} XIMText;

encode_is_wcharのとき、wide_charとして文字列が来るが、今回はmulti_byteで来る場合のみ扱う(gtkでも対応していない)。
lengthはここでもバイト数ではなく、実際の文字の数である。
XIMFeedbackは各文字の装飾に関する情報だが、今回は割愛する。
なお、textNULLになる場合があり、それはchg_firstからchg_lengthまでの文字が削除されたことを意味する。
C言語にはマルチバイト文字をいい感じに扱うライブラリがないため、真面目に仮入力中の文字すべてを正しく把握しようとすると結構面倒そうなので、今回は来た情報だけを標準出力に吐くだけにしておく

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void preedit_draw_callback(
XIM xim,
XPointer client_data,
XIMPreeditDrawCallbackStruct *call_data)
{

printf("callback\n");
XIMText *xim_text = call_data->text;
if (xim_text != NULL)
{
printf("Draw callback string: %s, length: %d, first: %d, caret: %d\n", xim_text->string.multi_byte, call_data->chg_length, call_data->chg_first, call_data->caret);
}
else
{
printf("Draw callback string: (DELETED), length: %d, first: %d, caret: %d\n", call_data->chg_length, call_data->chg_first, call_data->caret);
}
}

最後のXNPreeditCaretCallbackcall_dataとして以下のような構造体として情報が来る。

1
2
3
4
5
typedef struct _XIMPreeditCaretCallbackStruct {
int position; /* Caret offset within pre-edit string */
XIMCaretDirection direction; /* Caret moves direction */
XIMCaretStyle style; /* Feedback of the caret */
} XIMPreeditCaretCallbackStruct;

カーソルのポジションと移動した方向、カーソルの表示の際の装飾情報が渡されてくる。文字列は変化していないので、文字列に関する情報はない。
以下のようなコールバック関数を定義しておく。

1
2
3
4
5
6
7
8
9
10
11
static void preedit_caret_callback(
XIM xim,
XPointer client_data,
XIMPreeditCaretCallbackStruct *call_data)
{
printf("preedit caret\n");
if (call_data != NULL)
{
printf("direction: %d position: %d\n", call_data->direction, call_data->position);
}
}

さて、これらをXICをつくるときのパラメータとして渡すために、XVaNestedListとして渡すには以下のようにする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
XIMCallback draw_callback;
draw_callback.client_data = NULL;
draw_callback.callback = (XIMProc)preedit_draw_callback;
XIMCallback start_callback;
start_callback.client_data = NULL;
start_callback.callback = (XIMProc)preedit_start_callback;
XIMCallback done_callback;
done_callback.client_data = NULL;
done_callback.callback = (XIMProc)preedit_done_callback;
XIMCallback caret_callback;
caret_callback.client_data = NULL;
caret_callback.callback = (XIMProc)preedit_caret_callback;
XVaNestedList preedit_attributes = XVaCreateNestedList(
0,
XNPreeditStartCallback, &start_callback,
XNPreeditDoneCallback, &done_callback,
XNPreeditDrawCallback, &draw_callback,
XNPreeditCaretCallback, &caret_callback,
NULL);

今回はclient_dataを使わないので、NULLを渡しておいた。ポインタで渡しているだけなので、XIMCallbackXICの生存中にちゃんと生きていなければならないことには注意
そして、XICをつくる部分のコードを以下のように書き換える。

1
2
3
4
5
XIC ic = XCreateIC(xim,
XNInputStyle, XIMPreeditCallbacks | XIMStatusNothing,
XNClientWindow, win,
XNPreeditAttributes, preedit_attributes,
NULL);

完成品はこちら

実行例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
preedit start
callback
Draw callback string: あ, length: 0, first: 0, caret: 1
callback
Draw callback string: あい, length: 1, first: 0, caret: 2
callback
Draw callback string: あいう, length: 2, first: 0, caret: 3
callback
Draw callback string: あいうえ, length: 3, first: 0, caret: 4
callback
Draw callback string: あいうえお, length: 4, first: 0, caret: 5
delievered string: あいうえお
callback
Draw callback string: (DELETED), length: 5, first: 0, caret: 0
preedit done
preedit start
callback
Draw callback string: お, length: 0, first: 0, caret: 1
callback
Draw callback string: おn, length: 1, first: 0, caret: 2
callback
Draw callback string: おの, length: 2, first: 0, caret: 2
callback
Draw callback string: おのr, length: 2, first: 0, caret: 3
callback
Draw callback string: おのれ, length: 3, first: 0, caret: 3
callback
Draw callback string: おのれn, length: 3, first: 0, caret: 4
callback
Draw callback string: おのれの, length: 4, first: 0, caret: 4
callback
Draw callback string: おのれのf, length: 4, first: 0, caret: 5
callback
Draw callback string: おのれのふ, length: 5, first: 0, caret: 5
callback
Draw callback string: おのれのふg, length: 5, first: 0, caret: 6
callback
Draw callback string: おのれのふが, length: 6, first: 0, caret: 6
callback
Draw callback string: おのれのふがk, length: 6, first: 0, caret: 7
callback
Draw callback string: おのれのふがく, length: 7, first: 0, caret: 7
callback
Draw callback string: おのれのふがくw, length: 7, first: 0, caret: 8
callback
Draw callback string: おのれのふがくを, length: 8, first: 0, caret: 8
callback
Draw callback string: 己の富嶽を, length: 8, first: 0, caret: 0
callback
Draw callback string: 己の富嶽を, length: 5, first: 0, caret: 2
callback
Draw callback string: 己の富嶽を, length: 5, first: 0, caret: 0
callback
Draw callback string: おのれの富嶽を, length: 5, first: 0, caret: 0
callback
Draw callback string: 己の富嶽を, length: 7, first: 0, caret: 0
callback
Draw callback string: 己の富嶽を, length: 5, first: 0, caret: 2
callback
Draw callback string: 己の富岳を, length: 5, first: 0, caret: 2
callback
Draw callback string: 己のフガクを, length: 5, first: 0, caret: 2
callback
Draw callback string: 己の不学を, length: 6, first: 0, caret: 2
delievered string: 己の不学を
callback
Draw callback string: (DELETED), length: 5, first: 0, caret: 0
preedit done

リファレンス

一応、以上の仕様は以下のドキュメントにあるのだが、かなり古く読みにくい

概念的な話はこちら。正直、読み方がわからなかった

こっちはXlibの実装の解説ではあるが、レイアウトがメチャクチャである

Xlib − C Language X Interface

結局GTKの実装を参照するほうが楽であったが、GTK4(まだリリースはされていない)の最新版ではなんとXIMのサポートが削除されている。

GTK3までならちゃんと生きているので、そちらを参考にすると良い