iPadのリモートデスクトップで日本語が二重になるので、直すツールを作りました

私は絵を描く兼ね合いで、iPadを持ち歩いているのですが、PC作業がしたい!ってときはノートPCを追加で持ち歩くのは重たいので、iPad から自宅 WindowsPC へリモートデスクトップ接続をする方式を採用しています。

で、この iPad + リモートデスクトップという組み合わせで作業すると、

iPadから打つと文字が増える

日本語入力、特に漢字に変換したときにうまく文字が入ってくれません。

たとえば「変換が入るとおかしくなる。」と打ちたいのに、画面にはこう出ます。

へんかんがはいるとおか変換入るとおかしくなる。

読み(ひらがな)の残骸と変換結果が混ざってる状態ですね。

この症状、結構前から観測されているんですが、少しずつマシになってて、自分が試した範囲だと、最近はPowerShellみたいなターミナルやブラウザではちゃんと入るようになりました。

でもメモ帳を筆頭に、一部のアプリや一部のサイトでは今も壊れます。

というわけで、この症状を補正するWindows常駐ツール KanaFix をRustで作りました。

原因を実測してみた

まず何が起きてるのかをちゃんと見ることにしました。素のWin32ウィンドウに届いたメッセージをひたすら書き出すだけのプログラムを用意して、iPadから日本語を打ってみます。

分かったことは3つです。

1. PC側のIMEが動いてない

届く文字はすべて VK_PACKET(仮想キーコード 0xE7)を伴う WM_CHAR でした。WM_IME_STARTCOMPOSITION のようなIME関連のメッセージは1つも来ません。

VK_PACKETSendInputKEYEVENTF_UNICODE の組み合わせが生成するものです。SendInput はキーボードやマウスの操作をプログラムから擬似的に送り込むWin32 APIで、KEYEVENTF_UNICODE は「これは仮想キーじゃなくUnicode文字そのものだよ」と指定するフラグ。

つまり、iPad側で日本語変換を済ませてから、確定済みのUnicodeをWindowsへ流し込んでるわけです。ホスト側の変換エンジン(TSF / IMM32)はまるっとバイパスされてました。

まあ、あたりまえっちゃあたりまえです。変換してるのはiPadのキーボードなので。

2. 打って、消して、打ち直す

記録を時系列に並べ直すと、入力はこんな手順を踏んでました。

VK_PACKET  こ ん に ち は     ← 読みが実文字として注入される
Backspace x5                  ← 読みを消去
VK_PACKET  今 日 は 仕 事     ← 変換結果を注入
Backspace x6
VK_PACKET  も し

Backspaceの件数は記録のままです。抜粋なので、件数に対応する読みの注入行が抜けてる箇所があります。

iPadはホストアプリの中に変換候補ウィンドウを描けません。なので、いったん読みを確定文字として打ち込んで、それを合成Backspaceで消してから、変換後のテキストを注入し直す感じ。

3. メモ帳が追いつけない理由

旧来のWin32エディットコントロールや大半のアプリは、この「Backspaceして打ち直す」をちゃんと処理してくれます。読みはきれいに消えて、変換結果に置き換わります。

ところが新しいメモ帳(TSF / RichEditベース)は、ミリ秒単位で飛んでくる消去と注入のバーストを処理し切れません。読みを消し終わる前に変換結果が入ってきて、結果として二重化する。これが原因でした。

バーストを間引く

原因が分かればやることは単純です。触れる対象は、注入されてくる VK_PACKETBackspace のストリームだけ。ホスト側の変換はそもそも動いてないので、直しようがありません。

メモ帳に届く前に横取りして、エディタが追いつける速さに整えてあげることにしましょう。

KanaFixはグローバルな低レベルキーボードフック(WH_KEYBOARD_LL)を使って、対象アプリが前面にいるときだけイベントを捕まえ、ワーカースレッドから一定間隔(既定6ms)で再注入します。

WH_KEYBOARD_LLSetWindowsHookEx で仕掛けるフックの種類で、キー入力が各アプリに届く に、システム全体の入力を横取りできるしくみです。捨てたいイベントはフック関数から非ゼロを返して打ち切り、素通しでいいものは CallNextHookEx で次へ渡します。

ちなみにこのRDP経由の入力、合成入力のはずなのに LLKHF_INJECTED フラグが立ちません。判別に使えないので、VK_PACKET そのものを合成入力の目印として扱ってます。

確定バーストをほんの少し引き伸ばしてやると、メモ帳のTSFが消去と注入の競合に負けなくなる、という理屈です。

メモ帳のプロセスにDLLを注入したりはしません。再注入したイベントには dwExtraInfo(イベントに好きな目印を付けられる欄)に印を打っておいて、自分が流したものを自分でまた拾うフィードバックループを防いでます。

使い方

  1. GitHub Releases から kanafix.exe を入手します。
  2. 実行する前に、ハッシュを確かめてブロックを外します(下記)。
  3. kanafix.exe を実行します(初回に設定ファイル kanafix.toml を同じ場所へ自動生成します)。
  4. 対象アプリ(既定はメモ帳)で日本語を入力します。
  5. 終了は起動したコンソールウィンドウで Ctrl+C。止めれば補正は完全に無効化されます。

実行する前に

kanafix.exe はまだコード署名してないので、WindowsがSmartScreenや「不明な発行元」の警告を出します。素性の知れないexeを署名なしで配ってる自覚はあるので、走らせる前にハッシュだけは見てもらえると助かります。

# 1. 配布ハッシュと一致するか検証する
Get-FileHash .\kanafix.exe -Algorithm SHA256
#   出力されたハッシュが Releases の SHA256SUMS.txt と一致することを確認

# 2. ダウンロード由来のブロック(Mark of the Web)を解除する
Unblock-File .\kanafix.exe

ハッシュが SHA256SUMS.txt と一致していれば、SmartScreenの画面から「詳細情報」→「実行」で起動できます。

設定ファイル

apps = ["notepad.exe"]   # 補正したいアプリのexe名。複数指定できます
pace_ms = 6              # 再注入の間隔(ms)。直り切らなければ増やす(例: 12, 20)

apps に書いた名前がフォアグラウンドのプロセス名と大小無視で一致したときだけ補正します。それ以外のアプリや、他のキーには一切触れません。

冒頭に書いた「一部のサイトで壊れる」やつも、ここにブラウザのexe名を足せば同じ補正がかかります。ただしサイト側の入力欄の作りによるので、効くかどうかは試してみてください。

pace_ms は大きいほど安全側です。そのぶん入力の反映がほんのわずか遅くなります。環境によって直り切らないときは、ここを少しずつ増やしてみてください。

安全性について

グローバルキーボードフックは使いますが、キーストロークの記録も保存もしません。横取りしたイベントをそのまま送り直すだけで、ファイルにもネットワークにも書き出しません。

副作用がひとつあります。Backspaceは発生源を区別できないので、対象アプリが前面にある間は物理キーボードで押したBackspaceも同じ経路を通ります。1件ずつ間隔を空けて送り直すぶん、押しっぱなしにしたときの連続削除がわずかに遅くなります。

あと、管理者権限で動くアプリを対象にする場合は、kanafixも管理者として起動してください。権限が下のプロセスから送り直した入力は、対象アプリのウィンドウに届きません。

入力を一度奪って打ち直す方式なので、もし挙動が不安定になったらCtrl+Cで即停止すれば素の入力に戻ります。

中身が気になる方はソースを読んでみてください。

まとめ

  • iPadのWindows Appから日本語を打つと、変換がiPad側で完結して、確定済みのUnicodeが注入される
  • そのときの「読みを打つ→Backspaceで消す→打ち直す」バーストを新しいメモ帳が処理し切れず、文字が二重化する
  • KanaFixは対象アプリが前面のときだけこのバーストを横取りして間隔を空け直し、エディタが追いつけるようにする

同じ症状で困ってる人の役に立てば嬉しいです。要望やバグ報告、質問はX @pan_afrayz へどうぞ。