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で再実装されたもの。
構造は以下
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行で変換できます)