Open3

Rust for Windows

たけぴょんたけぴょん

Win32APIでキーボードのシステムフックをコールバック取得
チャンネルでコールバックから送信して別スレッドで受信するコード例

use std::sync::OnceLock;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::mpsc;
use std::sync::Mutex;
use std::thread;
use std::time::Duration;

use windows::Win32::Foundation::*;
use windows::Win32::UI::WindowsAndMessaging::*;

static RX: OnceLock<Mutex<Receiver<u16>>> = OnceLock::new();
static TX: OnceLock<Mutex<Sender<u16>>> = OnceLock::new();

fn init_channel() {
    let (tx, rx) = mpsc::channel();
    RX.set(Mutex::new(rx)).unwrap();
    TX.set(Mutex::new(tx)).unwrap();
}
unsafe fn extract_rawkey(lparam: &LPARAM) -> &u16 {
    &*(lparam.0 as *const u16) as &u16
}

extern "system" fn keyboard_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        if ncode as u32 == HC_ACTION {
            match wparam.0 as u32 {
                WM_KEYDOWN => {
                    let tx = TX.get()
                        .expect("Failed to get")
                        .lock()
                        .expect("Failed to lock")
                        .clone();
                    tx.send(*extract_rawkey(&lparam)).expect("Failed send");
                    println!("Send Keydown {}", *extract_rawkey(&lparam));
                }
                WM_KEYUP => {
                    let tx = TX.get()
                        .expect("Failed to get")
                        .lock()
                        .expect("Failed to lock")
                        .clone();
                    tx.send(*extract_rawkey(&lparam)).expect("Falied send");
                    println!("Send Keyup {}", *extract_rawkey(&lparam));
                }
                _ => {
                    let tx = TX.get()
                        .expect("Failed to get")
                        .lock()
                        .expect("Failed to lock")
                        .clone();
                    tx.send(*extract_rawkey(&lparam)).expect("Failed send");
                    println!("Send Other {}", *extract_rawkey(&lparam));
                }
            }
        }
        CallNextHookEx(HHOOK::default(), ncode, wparam, lparam)
    }
}

fn keyboad_hook() {
    unsafe {
        let k_hook = SetWindowsHookExA(
            WH_KEYBOARD_LL,
            Some(keyboard_proc),
            HINSTANCE::default(),
            0
        ).expect("Failed systemhok");
        let mut message = MSG::default();
        while GetMessageA(&mut message, HWND::default(), 0, 0).into() {
            TranslateMessage(&message).expect("Failed TranslateMessageA");
            DispatchMessageA(&message);
        }
        if !k_hook.is_invalid() {
            UnhookWindowsHookEx(k_hook).expect("Failed unhook");
        } else {
            panic!("Hook paniced");
        }
    }
}

fn main() {
    init_channel();
    let handle_hook = thread::spawn(|| {
        println!("Start hook");
        keyboad_hook();
        println!("End hook");
    });
    let handle_recv = thread::spawn(|| {
        println!("Waiting receive key");
        loop {
            match RX.get().expect("Failed get").lock().expect("Failed lock").recv_timeout(Duration::from_millis(500)) {
                Ok(recv) => println!("Received: {}", recv),
                Err(err) => {
                    println!("Timeout: {err}");
                }
            }
        }
    });
    handle_recv.join().expect("Failed join recv");
    handle_hook.join().expect("Failed join recv");
}
たけぴょんたけぴょん

システムフックのコールバック関数で取得したパラメータを構造体に変換してスキャンコードと仮想キーコードを取得する

fn keyboard_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULTについて

今回はkeyboard_procをキーボードのシステムフックのコールバックに設定しました。
受け取るWPARAMとLPARAMについて以下で解説

Windows - LowLevelKeyboardProc関数

WPARAM

  • WM_KEYDOWN
  • WM_KEYUP
  • WM_SYSKEYDOWN
  • WM_SYSKEYUP

のいずれかが入る。使うときはu32にキャストする。

LPARAM

こっちが大事。スキャンコードと仮想キーコードが含まれます。
元のコードではLPARAMをu16に変換しているだけだがlparam.0を生ポインタのKBDLLHOOKSTRUCTに変換することでフィールドを参照出来ます。
参照出来るように以下のようにextract_rawkeyを変更してみました。

unsafe fn extract_rawkey(lparam: &LPARAM) -> u32 {
    let kbdhook = lparam.0 as *const KBDLLHOOKSTRUCT;
    let vkcode = (*kbdhook).vkCode; 
    let scancode = (*kbdhook).scanCode;
    let flags = (*kbdhook).flags;
    let time = (*kbdhook).time;
    let exinfo = (*kbdhook).dwExtraInfo;
    println!("vkCode: {}, ScanCode: {}", vkcode, scancode);
    scancode
}

KBDHOOKSTRUCTはWin32APIでの構造体がRustで再実装されたもの。
構造は以下

C++
typedef struct tagKBDLLHOOKSTRUCT {
  DWORD     vkCode;
  DWORD     scanCode;
  DWORD     flags;
  DWORD     time;
  ULONG_PTR dwExtraInfo;
} KBDLLHOOKSTRUCT, *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;

今回取得したいのは仮想キーコードvkCodeとスキャンコードscanCodeです。

スキャンコードと仮想キーコード

画像引用: https://learn.microsoft.com/ja-jp/windows/win32/inputdev/about-keyboard-input

スキャンコードはキーボードのキーひとつひとつのコードで、文字(Aとか)を表すわけではありません。例えばESCキーだと1を表します。
OSはスキャンコードを受取、その後キーレイアウトとかキーマップと言われるものに変換します。この結果がAとか@とかの文字に対応します。

実験してみましたがWindowsではLPARAMのvkCodeで文字を取得できるようです。USキーレイアウトとかにも変えてみました。Linuxだとグローバルにキーを取得しようとするとスキャンコードしか取れずキーマップでいちいち変換しないといけないのでちょっと楽ですね。(とはいえxkbcommonのおかげで1,2行で変換できます)