起因
最近在用 electron 开发一个线上的考试应用,有一个需求是在监考模式下拦截 用户的键盘事件,防止用户锁屏或者使用其它快捷键切换应用,在 github 翻了一圈,没有找到合适的方案 o(╥﹏╥)o,所以用 rust 写了一个 lib,然后用 napi-rs 打包成 nodejs 库。下面记录下整个流程。
mac os
开发 mac os 应用,需要使用 cocoa 库,mozilla 开发了一个 rust 版本的库core-foundation-rs
使用 core_graphics 的 CGEventTap 可以拦截键盘事件
_16use core_graphics::event::{_16 CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType::KeyDown,_16};_16_16fn handle_key_event() {_16 let cg_event_tap = CGEventTap::new(_16 CGEventTapLocation::Session,_16 // 插入队首_16 CGEventTapPlacement::HeadInsertEventTap,_16 // 默认行为过滤_16 CGEventTapOptions::Default,_16 // 监听的事件_16 vec![CGEventType::KeyDown],_16 callback,_16 );_16}
构造 CGEventTap 的 5 参数依次表示
- 监听事件的区域 直接选 CGEventTapLocation::Session 即可
- 插入队首还是队尾,当一个事件触发的时候,会依次调用回掉队列上面的回调函数,前面的回调函数可以阻止后面的回调函数触发,这里插入队首部,阻止后面的默认系统行为触发
- 只监听事件的触发还是可以阻止后面的行为触发 这里选择 Default
- 监听的事件,当前只监听键盘 KeyDown 事件
- 回调函数
callback
callback 接受 3 个参数,分别是 CGEventTapProxy, CGEventType, CGEvent
添加 callback 函数
callback 函数可以
判断 event 类型
event 类型需要通过判断当前的 CGEventType 是否
获取 keycode
通过 get_integer_value_field 拿到 keycode
判断是否是 meta 键
通过 event.get_flags() 判断 CGEventFlags
禁用某个键
设置 key event 为 NULL , 禁用这个键,比如禁用 control
上面的代码,还要加入事件循环中才能生效
_19use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop};_19_19let current: CFRunLoop = CFRunLoop::get_current();_19_19match cg_event_tap {_19 Ok(tap) => unsafe {_19 let loop_source = tap_19 .mach_port_19 .create_runloop_source(0)_19 .expect("sames broken");_19_19 current.add_source(&loop_source, kCFRunLoopCommonModes.clone());_19_19 tap.enable();_19_19 CFRunLoop::run_current();_19 },_19 Err(_) => panic!("can't prevent key event"),_19}
最终得到在 mac os 下的代码如下所示
在 mac os 需要在隐私和安全性里面打开对应程序的辅助功能,该程序才能生效
_51use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop};_51use core_graphics::event::{_51 CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,EventField,CGEventFlags,_51};_51_51fn handle_key_event() {_51 let cg_event_tap = CGEventTap::new(_51 CGEventTapLocation::Session,_51 // 插入队首_51 CGEventTapPlacement::HeadInsertEventTap,_51 // 默认行为过滤_51 CGEventTapOptions::Default,_51 // 监听的事件_51 vec![CGEventType::KeyDown],_51 |proxy: *const std::ffi::c_void, event_type: CGEventType, event: &CGEvent| match event_type {_51 CGEventType::KeyDown => {_51 let key = event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);_51_51 // 判断 control 键是否摁下_51 let is_control_press =_51 CGEventFlags::CGEventFlagControl & event.get_flags() == CGEventFlags::CGEventFlagControl;_51_51 if is_control_press {_51 // 禁用这个快捷键_51 event.set_type(CGEventType::Null);_51 }_51_51 Some(event.clone())_51 }_51 _ => Some(event.clone()),_51 },_51 );_51_51 let current: CFRunLoop = CFRunLoop::get_current();_51_51 match cg_event_tap {_51 Ok(tap) => unsafe {_51 let loop_source = tap_51 .mach_port_51 .create_runloop_source(0)_51 .expect("sames broken");_51_51 current.add_source(&loop_source, kCFRunLoopCommonModes.clone());_51_51 tap.enable();_51_51 CFRunLoop::run_current();_51 },_51 Err(_) => panic!("can't prevent key event"),_51 }_51}
windows
windows 下面实现锁屏的功能实在有点复杂,除了写键盘的事件 hook , 还要修改用户的注册表
这里使用了一个第三方库rdev来实现,rdev本质上还是调用了 windows api SetWindowsHookExA 来实现的
微软官方维护了一个 windows-rs 的 crate,也可以通过它来实现相应的功能
具体使用 rdev grab 的代码就不再详细说了
_10rdev::grab(move |ev| match ev.event_type {_10 EventType::KeyPress(key) => {_10 if should_restrict(key) {_10 None_10 } else {_10 Some(ev)_10 }_10 }_10 _ => Some(ev),_10});
rdev 可以拦截绝大部分 windows 的按键,但是有一些键很特殊,是不能通过写 hook 来拦截的,比如这三个键
win + l锁屏win + g弹出 windows gamectrl + alt + deleteCAD
上面三个键只能通过修改用户的注册表来实现拦截
修改注册表需要 windows 的管理员权限,必须要请求管理员权限
win + l 的注册表位于 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\System 将 DisableLockWorkstation 改为 1 即可禁用锁屏
win + g 位于 HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR 将 AppCaptureEnabled 设置成 0 即可禁止
cad 位于 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\taskmgr.exe 将 Debugger 设置成 Hotkey Disabled 即可禁止
如果是在 electron 里面直接使用 nodejs 的库 regedit 即可,使用 regedit 禁用 win + l 示例如下
_24// 禁用 win + l 锁屏_24import regeditOrigin from "regedit"_24_24const regedit = regeditOrigin.promisified_24_24async function main() {_24 const winLRegisterPath =_24 "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"_24_24 try {_24 await regedit.createKey([winLRegisterPath])_24 } catch (error) {_24 console.error(error)_24 }_24_24 await regedit.putValue({_24 [winLRegisterPath]: {_24 DisableLockWorkstation: {_24 type: "REG_DWORD",_24 value: 1,_24 },_24 },_24 })_24}
regedit 在调用的时候需要将 HKEY_CURRENT_USER 需要改成 HKCU
如法炮制即可禁用剩余的 win 快捷键,使用 napi-rs 打包, 用 github actions 编译成 .node 文件, 便可在 electron 中使用实现 霸屏 的功能
真不好搞 😭