はじめに
過去、こういう記事を書きました。
この時から構想はあったので、長い冬休みの間で調べて作ってみた。
リポジトリ
何を作ったのか、ていうかそもそもどういう仕組みなのかについては上記の記事を読むと良いかも。
操作付きで説明しているので多分わかってもらえる
構想の概要
そもそも、Windowsというか大体のOSはメッセージキューを持っています。
キー入力やマウス入力、時刻の測定をするためのタイマーやなんかも使っていたりします。
なので、ABC とキーボードを叩くと以下のようにメッセージキューを通り、現在アクティブなアプリに対してキー入力ができるわけです。
このメッセージキューに対して何らかの操作をすれば行けそうだなというのはなんとなく知っていたので色々試しました。
そもそもなぜやろうと思ったのか
初期のツール、というかClipboard経由のペーストの仕組みは以下のような感じです。
アプリを起動して、複数行のテキストをコピーしてペーストをする。
もちろんこれで目的は達成しました。
ただ、この方式は上記の画像で説明している通り、「クリップボード」に常にコピーされ続けます。
そのため、クリップボードの履歴がすべて上書きされます。
また、ペースト操作がJavaScriptなどで禁止されている場合、入力できません。
上記の2つのデメリットが存在するわけです。
なので、手法としては2種類用意しておいたほうが良いなと思ったので作ることにしました。
新しい仕組み(キーボード入力のエミュレーション)
これをいい感じに実装するために、色々試したのでそれを書きます。
[失敗]SendMessage(WM_CHAR、WM_IME_CHAR)での実装
結論から書くとうまくいきませんでした。
そもそも使うべきAPIが違うっていうことですね。
// cb:クリップボードに存在した文字列データ、型は VecDeque<String>
// 1行ごとにVecの1要素として保存している。(aa\nbbの場合、cb=["aa","bb"];となっている
let data = OsString::from(cb.pop_back().unwrap())
.encode_wide()
.collect::<Vec<u16>>();
let strdata_len = data.len() * 2;
let data_ptr = data.as_ptr();
let gdata = GlobalAlloc(GHND | GLOBAL_ALLOC_FLAGS(GMEM_SHARE), strdata_len + 2);
let locked_data = GlobalLock(gdata);
// WM_SETTEXTは、タイトルバーのテキストの書き換えがされてしまった。(メモ帳の場合「無題 - メモ帳)のところ」
// まぁこれはこれでなにかに使えそうなのでいいけど。
SendMessageW(HWND(active_window.0), WM_SETTEXT, WMARAM(0), LPARAM(locked_data));
// 以下は、WM_CHARシリーズ、何も反応せんかった。
for c in data {
SendMessageW(HWND(active_window.0), WM_CHAR, WMARAM(c), LPARAM(0));
SendMessageW(HWND(active_window.0), WM_IME_CHAR, WMARAM(c), LPARAM(0));
}
GlobalUnlock(gdata);
GlobalFree(gdata);
[成功]SendInputによるキーボード入力エミュレーション
こっちはうまくいきました。
本来はこっちを使うべきでした。
調査対象はこれ。
KeePass2はいい感じにキーボードエミュレーションをしていたので、そこの部分を解析すりゃなんかいけんじゃねぇか?
って思って読んでみた。
そしたら以下のコードがビンゴだった。
ソースコードはこちら
”ものぐさ”な方々へ、どこに有るのかっていうのをペタリ。
// SiEngineWin.cs
private bool SendVKeyNative64(int vKey, bool? obExtKey, char? optUnicodeChar,
bool bDown)
{
NativeMethods.SpecializedKeyboardINPUT64[] pInput = new
NativeMethods.SpecializedKeyboardINPUT64[1];
pInput[0].Type = NativeMethods.INPUT_KEYBOARD;
if(optUnicodeChar.HasValue && WinUtil.IsAtLeastWindows2000)
{
pInput[0].VirtualKeyCode = 0;
pInput[0].ScanCode = (ushort)optUnicodeChar.Value;
pInput[0].Flags = ((bDown ? 0 : NativeMethods.KEYEVENTF_KEYUP) |
NativeMethods.KEYEVENTF_UNICODE);
}
else
{
IntPtr hKL = m_swiCurrent.KeyboardLayout;
if(optUnicodeChar.HasValue)
vKey = (int)(NativeMethods.VkKeyScan3(optUnicodeChar.Value,
hKL) & 0xFFU);
pInput[0].VirtualKeyCode = (ushort)vKey;
pInput[0].ScanCode = (ushort)(NativeMethods.MapVirtualKey3(
(uint)vKey, NativeMethods.MAPVK_VK_TO_VSC, hKL) & 0xFFU);
pInput[0].Flags = GetKeyEventFlags(vKey, obExtKey, bDown);
}
pInput[0].Time = 0;
pInput[0].ExtraInfo = NativeMethods.GetMessageExtraInfo();
Debug.Assert(Marshal.SizeOf(typeof(NativeMethods.SpecializedKeyboardINPUT64)) == 40);
int size = Marshal.SizeOf(typeof(NativeMethods.SpecializedKeyboardINPUT64));
// ここの行がSendInputAPIを呼び出しているところ。
if (NativeMethods.SendInput64Special(1, pInput,
Marshal.SizeOf(typeof(NativeMethods.SpecializedKeyboardINPUT64))) != 1)
return false;
return true;
}
そんなわけで上記を参考にして書いたコードがこちら。
fn send_key_input(c: u16) {
unsafe {
let mut kbd = KEYBDINPUT::default();
kbd.wVk = VIRTUAL_KEY(0);
kbd.wScan = c;
kbd.dwFlags = KEYEVENTF_UNICODE;
kbd.time = 0;
kbd.dwExtraInfo = GetMessageExtraInfo().0 as usize;
let mut input = INPUT::default();
input.r#type = INPUT_KEYBOARD;
input.Anonymous.ki = kbd;
let result = SendInput(&[input], std::mem::size_of::<INPUT>() as i32);
}
}
C/C++やC#での実装は結構あるんですが、Rustで実装してる人あんまり居なくて調査に時間がかかった。
思いつきでやるもんじゃないですねー
でもできたからいいや。
じゃあ今日はここまで。
補足事項
本記事のコードではコンビネーションキー(CTRL等)と同時に使うことを考慮していません。
もし、CTRL+Vをしたタイミングで走る本アプリのような物に実装する場合は
CTRLが押されていたら離すコードを送る、キーコードを送る、CTRLを押すコードを送る
というような流れ出ないと動きません(すべてショートカットと見做されます)