14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rust で windows クレートを使ってキーボード入力をバックグラウンドから取得して表示する

Last updated at Posted at 2022-02-13

動機

  • Win32API を使ってみたい
  • Rust を使って何かしたい

RustでWindows用キー入力表示ソフトを作ってみるか。

環境

エディション	Windows 10 Pro
バージョン	21H1
インストール日	‎2021/‎02/‎02
OS ビルド	19043.1526
エクスペリエンス	Windows Feature Experience Pack 120.2212.4170.0
nightly-x86_64-pc-windows-msvc (default)
rustc 1.60.0-nightly (e646f3d2a 2022-02-10)

1. windowsクレートを使ったウィンドウ有りサンプルを実行する

Win32APIをRustから使用する際、GitHugの microsoft/windows-rs で管理されている windows クレートが使えます。

samples ディレクトリがあるので、良さげな windows-rs/crates/samples/create_window/ を手元で実行してみます。

cargo new してプロジェクトを作ります。

cargo new keyboard-input

サンプルの Cargo.toml はクレートを相対パスで指定しているので、現時点の最新 version 0.32.0 を指定します。

Cargo.toml
[package]
name = "keyboard-input"
version = "0.1.0"
edition = "2021"

[dependencies.windows]
version = "0.32.0"
features = [
    "alloc",
    "Win32_Foundation",
    "Win32_Graphics_Gdi",
    "Win32_System_LibraryLoader",
    "Win32_UI_WindowsAndMessaging",
]

src/main.rs はそのままコピペします。

src/main.rs
use windows::{core::*, Win32::Foundation::*, Win32::Graphics::Gdi::ValidateRect, Win32::System::LibraryLoader::GetModuleHandleA, Win32::UI::WindowsAndMessaging::*};

fn main() -> Result<()> {
    unsafe {
        let instance = GetModuleHandleA(None);
        debug_assert!(instance.0 != 0);

        let window_class = "window";

        let wc = WNDCLASSA {
            hCursor: LoadCursorW(None, IDC_ARROW),
            hInstance: instance,
            lpszClassName: PSTR(b"window\0".as_ptr()),

            style: CS_HREDRAW | CS_VREDRAW,
            lpfnWndProc: Some(wndproc),
            ..Default::default()
        };

        let atom = RegisterClassA(&wc);
        debug_assert!(atom != 0);

        CreateWindowExA(Default::default(), window_class, "This is a sample window", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, None, None, instance, std::ptr::null_mut());

        let mut message = MSG::default();

        while GetMessageA(&mut message, HWND(0), 0, 0).into() {
            DispatchMessageA(&message);
        }

        Ok(())
    }
}

extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        match message as u32 {
            WM_PAINT => {
                println!("WM_PAINT");
                ValidateRect(window, std::ptr::null());
                LRESULT(0)
            }
            WM_DESTROY => {
                println!("WM_DESTROY");
                PostQuitMessage(0);
                LRESULT(0)
            }
            _ => DefWindowProcA(window, message, wparam, lparam),
        }
    }
}

cargo run するとウィンドウが出現します。windows クレートを使用することができました。

image.png

ウィンドウをドラッグ・リサイズするとターミナルに WM_PAINT が出力され、ウィンドウを閉じると WM_DESTROY が出力されます。

下の関数 extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT がコールバック関数で、ウィンドウの描写と削除を監視しています。

2. フォアグラウンドでキーボード入力を取得する

C++ win32 get keyboard で検索すると、Microsoft Docs の キーボード入力 (Win32 および C++ でのはじめに) - Win32 apps が見つかり、LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) から始まるコールバック部分に適用できそうなサンプルコードがあるので移植します。

src/main.rs
// 略

extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        match message as u32 {
            WM_PAINT => {
                println!("WM_PAINT");
                ValidateRect(window, std::ptr::null());
                return LRESULT(0);
            }
            WM_DESTROY => {
                println!("WM_DESTROY");
                PostQuitMessage(0);
                return LRESULT(0);
            }
            WM_KEYDOWN => {
                println!("WM_KEYDOWN: {}", wparam.0);
            }
            WM_KEYUP => {
                println!("WM_KEYUP: {}", wparam.0);
            }
            _ => ()
        }
        DefWindowProcA(window, message, wparam, lparam)
    }
}

cargo run してからキーボードで abc と入力すると、

PS P:\program\rust\keyboard-input> cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target\debug\keyboard-input.exe`
WM_PAINT
WM_KEYDOWN: 65
WM_KEYUP: 65
WM_KEYDOWN: 66
WM_KEYUP: 66
WM_KEYDOWN: 67
WM_KEYUP: 67
WM_DESTROY
PS P:\program\rust\keyboard-input> 

対応する文字コードの値が表示されます。

ここで問題があり、ウィンドウからフォーカスを外す(別のウィンドウにフォーカスする)と、ターミナル上に出力がされません。これではキー入力表示ソフトには成りません。

3. バックグラウンドからキーボード入力を取得する

C++ win32 background get keyboard で検索します。

C++/Win32: Keyboard input to a non-foreground window - Stack Overflow

有りました。貼ってあるGIF を見る限り実装したい機能そのものです。

シンプルなコードなので、以下の方針で移植します。

  • C++でNULLとなっていて、Rust上では構造体が要求される箇所は T::default() にする
  • Rust上の構造体とプリミティブ型の比較をする際は構造体の中身を使う
src/main.rs
use windows::{core::*, Win32::Foundation::*, Win32::UI::WindowsAndMessaging::*};

fn main() -> Result<()> {
    unsafe {
        let k_hook = SetWindowsHookExA(WH_KEYBOARD_LL, Some(k_callback1), HINSTANCE::default(), 0);
        let mut message = MSG::default();
        while GetMessageA(&mut message, HWND::default(), 0, 0).into() {
            DispatchMessageA(&message);
        }
        if !k_hook.is_invalid() {
            UnhookWindowsHookEx(k_hook);
        }
        Ok(())
    }
}

extern "system" fn k_callback1(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        if wparam.0 as u32 == WM_KEYDOWN && ncode as u32 == HC_ACTION {
            let vk_code_inner = &*(lparam.0 as *const u16) as &u16;
            dbg!(vk_code_inner);
        }
        CallNextHookEx(HHOOK::default(), ncode, wparam, lparam)
    }
}

keyboard-input.gif

移植時に悩んだ部分

extern

k_callback1 の extern 部分をC++の __stdcall に対応するよう extern "stdcall" にすると

PS P:\program\rust\keyboard-input> cargo run
   Compiling keyboard-input v0.1.0 (P:\program\rust\keyboard-input)
error[E0308]: mismatched types
 --> src\main.rs:5:61
  |
5 |         let k_hook = SetWindowsHookExA(WH_KEYBOARD_LL, Some(k_callback1), HINSTANCE::default(), 0);
  |                                                             ^^^^^^^^^^^ expected "system" fn, found "stdcall" fn
  |
  = note: expected fn pointer `unsafe extern "system" fn(_, windows::Win32::Foundation::WPARAM, windows::Win32::Foundation::LPARAM) -> windows::Win32::Foundation::LRESULT`
                found fn item `extern "stdcall" fn(_, windows::Win32::Foundation::WPARAM, windows::Win32::Foundation::LPARAM) -> windows::Win32::Foundation::LRESULT {k_callback1}`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `keyboard-input` due to previous error

このように型が合わなくなります。

External blocks - The Rust Reference に以下のように書いてあります。

extern "system" -- Usually the same as extern "C", except on Win32, in which case it's "stdcall", or what you should use to link to the Windows API itself
// 略
extern "stdcall" -- The default for the Win32 API on x86_32.

x86_64 なら、expected されている extern "system" として問題ないはずです。

構造体のキャスト

C++の PKBDLLHOOKSTRUCT key = (PKBDLLHOOKSTRUCT)lParam; に対応させて、以下のようにするとコンパイルできません。

let hookStruct = &*(lparam as *const KBDLLHOOKSTRUCT) as &KBDLLHOOKSTRUCT;
PS P:\program\rust\keyboard-input> cargo run
   Compiling keyboard-input v0.1.0 (P:\program\rust\keyboard-input)
error[E0605]: non-primitive cast: `windows::Win32::Foundation::LPARAM` as `*const windows::Win32::UI::WindowsAndMessaging::KBDLLHOOKSTRUCT`
  --> src\main.rs:19:28
   |
19 |         let hookStruct = &*(lparam as *const KBDLLHOOKSTRUCT) as &KBDLLHOOKSTRUCT;
   |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object

For more information about this error, try `rustc --explain E0605`.
error: could not compile `keyboard-input` due to previous error

as はプリミティブ型間か特定のトレイトオブジェクトに変換する場合にしか使えないと叱られます。

KBDLLHOOKSTRUCT にキャストしていますが、使用しているのは vkCode だけです。KBDLLHOOKSTRUCT (winuser.h) - Win32 apps を読むと vkCode は 構造体の先頭 DWORD (符号なし32ビット整数) なので、以下のようにしました。

let vk_code_inner = &*(lparam.0 as *const u16) as &u16;

(2022-02-18 追記)
@benki さんに コメントstd::mem::transmute を使用すればキャストできると教えていただきました。

let kb_hook = std::mem::transmute::<isize, &KBDLLHOOKSTRUCT>(lparam.0);
dbg!(kb_hook.vkCode);

ドキュメントには信じられないほど unsafe とあるので、気をつけて使うと良さそうです。

transmute is incredibly unsafe. There are a vast number of ways to cause undefined behavior with this function. transmute should be the absolute last resort.

4. バックグラウンドからキーボード入力を取得して表示する

上記のコードでは wparam.0 as u32 == WM_KEYDOWN としているので、キーが離れたときの検知ができていません。

現在のキー入力状態を保存して表示するように修正します。

src/main.rs
use windows::{core::*, Win32::Foundation::*, Win32::UI::WindowsAndMessaging::*};

fn main() -> Result<()> {
    unsafe {
        let k_hook = SetWindowsHookExA(WH_KEYBOARD_LL, Some(k_callback1), HINSTANCE::default(), 0);
        let mut message = MSG::default();
        while GetMessageA(&mut message, HWND::default(), 0, 0).into() {
            DispatchMessageA(&message);
        }
        if !k_hook.is_invalid() {
            UnhookWindowsHookEx(k_hook);
        }
        Ok(())
    }
}

static mut INPUTS_ARRAY: [bool; 256] = [false; 256];
unsafe fn set_and_show(vk_code: &u16, tf: bool) {
    INPUTS_ARRAY[*vk_code as usize] = tf;
    let mut s = String::with_capacity((b'Z' - b'A' + 1) as usize);
    for i in (b'A' as usize)..=(b'Z' as usize) {
        s.push(if INPUTS_ARRAY[i] { 'T' } else { 'F' });
    }
    println!("{s}");
}

extern "system" fn k_callback1(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        if ncode as u32 == HC_ACTION {
            match wparam.0 as u32 {
                WM_KEYDOWN => set_and_show(&*(lparam.0 as *const u16) as &u16, true),
                WM_KEYUP => set_and_show(&*(lparam.0 as *const u16) as &u16, false),
                _ => (),
            }
        }
        CallNextHookEx(HHOOK::default(), ncode, wparam, lparam)
    }
}

Aを押して離す→Aを押す→Bを押す→Cを押す→ABCを離す とした場合のGIFです。

keyboard-input2.gif

キーの入力開始と終了がバックグラウンドでも分かるようになりました。あとは 頑張って 表示を作れば良いはずです。別の記事で書くかもしれません。

あとがき

  • Win32APIは(C++の)サンプルコードが多くあるので、Rustで使うにしても結構どうにかなるかもしれない
  • 型があると型に従えば良いのでやりやすい
  • ユーザーが編集可能なキー入力表示ソフトにしようとした場合、かなり面倒そう
    • Win32APIのSetWindowsHookExAを使って入力を取れるRustのGUIライブラリがあるなら、それで作ったほうが楽そう
14
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?