起因
最近在用 electron
开发一个线上的考试应用, 有一个需求是客户端处于监考模式下要拦截用户的键盘事件. 这里的拦截是全局性的,但是 globalShortcut 很多行为都拦截不了, 比如快捷键锁屏 . 在 github
翻了一圈后,没有找到合适的方案 o(╥﹏╥)o,所以自己用 rust
写一个 lib
,然后用 napi-rs 打包成 nodejs
库在 electron
中使用。下面记录下整个流程, 方便参考。
mac os
如果你不关心 mac, 可以跳过这一块, 直接看 win
开发 mac os 应用,需要使用 cocoa 库,mozilla 开发了一个 rust
版本的库core-foundation-rs
使用它的 core_graphics
的 CGEventTap 可以拦截键盘事件
构造 CGEventTap
rust
use core_graphics::event::{
CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType::KeyDown,
};
fn handle_key_event() {
let cg_event_tap = CGEventTap::new(
CGEventTapLocation::Session,
// 插入队首
CGEventTapPlacement::HeadInsertEventTap,
// 默认行为过滤
CGEventTapOptions::Default,
// 监听的事件
vec![CGEventType::KeyDown],
callback,
);
}
构造 CGEventTap
的 5 个参数依次为
- 监听事件的区域 直接选
CGEventTapLocation::Session
即可 - 插入队首还是队尾,当一个事件触发的时候,会依次调用回掉队列上面的回调函数,前面的回调函数可以阻止后面的回调函数触发,这里插入队首部,阻止后面的默认系统行为触发
- 只监听事件的触发还是可以阻止后面的行为触发 这里选择
Default
- 监听的事件,当前只监听键盘
KeyDown
事件 - 回调函数
callback
添加 callback
函数判断 event
类型
event
类型需要通过判断当前的 CGEventType
rust
use core_graphics::event::{
CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,
};
fn handle_key_event() {
let cg_event_tap = CGEventTap::new(
CGEventTapLocation::Session,
// 插入队首
CGEventTapPlacement::HeadInsertEventTap,
// 默认行为过滤
CGEventTapOptions::Default,
// 监听的事件
vec![CGEventType::KeyDown],
|proxy: *const std::ffi::c_void, event_type: CGEventType, event: &CGEvent| match event_type {
CGEventType::KeyDown => {
Some(event.clone())
}
_ => Some(event.clone()),
},
);
}
获取 key 类型
通过 get_integer_value_field
拿到 keycode
通过 event.get_flags()
判断 CGEventFlags
rust
use core_graphics::event::{
CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,EventField,CGEventFlags,
};
fn handle_key_event() {
let cg_event_tap = CGEventTap::new(
CGEventTapLocation::Session,
// 插入队首
CGEventTapPlacement::HeadInsertEventTap,
// 默认行为过滤
CGEventTapOptions::Default,
// 监听的事件
vec![CGEventType::KeyDown],
|proxy: *const std::ffi::c_void, event_type: CGEventType, event: &CGEvent| match event_type {
CGEventType::KeyDown => {
let key = event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);
// 判断 control 键是否摁下
let is_control_press =
CGEventFlags::CGEventFlagControl & event.get_flags() == CGEventFlags::CGEventFlagControl;
Some(event.clone())
}
_ => Some(event.clone()),
},
);
}
禁用 control 键
设置 key event
为 NULL
, 就可以禁用这个键,比如禁用 control
rust
use core_graphics::event::{
CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,EventField,CGEventFlags,
};
fn handle_key_event() {
let cg_event_tap = CGEventTap::new(
CGEventTapLocation::Session,
// 插入队首
CGEventTapPlacement::HeadInsertEventTap,
// 默认行为过滤
CGEventTapOptions::Default,
// 监听的事件
vec![CGEventType::KeyDown],
|proxy: *const std::ffi::c_void, event_type: CGEventType, event: &CGEvent| match event_type {
CGEventType::KeyDown => {
let key = event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);
// 判断 control 键是否摁下
let is_control_press =
CGEventFlags::CGEventFlagControl & event.get_flags() == CGEventFlags::CGEventFlagControl;
if is_control_press {
// 禁用这个快捷键
event.set_type(CGEventType::Null);
}
Some(event.clone())
}
_ => Some(event.clone()),
},
);
}
加入循环队列
上面的代码,还要加入事件循环中才能生效
rust
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop};
let current: CFRunLoop = CFRunLoop::get_current();
match cg_event_tap {
Ok(tap) => unsafe {
let loop_source = tap
.mach_port
.create_runloop_source(0)
.expect("sames broken");
current.add_source(&loop_source, kCFRunLoopCommonModes.clone());
tap.enable();
CFRunLoop::run_current();
},
Err(_) => panic!("can't prevent key event"),
}
在 mac os 需要在隐私和安全性里面打开对应程序的辅助功能,该程序才能生效!!!
上面的代码可以在 mac os
下禁用 control
键, 目前的 globalShortcut
实现不了这个功能
win
windows
下面实现该功能实在有点复杂,除了写键盘的事件 hook
, 还要修改用户的注册表
这里拦截键盘事件使用了一个第三方库rdev来实现,rdev
本质上还是调用了 windows api
SetWindowsHookExA
来实现的, 因为用 windows api
很麻烦, 为了省事, 这里直接用 rdev
微软官方维护了一个 windows-rs 的 crate
,也可以通过它来实现相应的功能
rdev 拦截键盘事件
具体使用 rdev grab
的代码就不再详细说了, 可以去看它的文档
rust
rdev::grab(move |ev| match ev.event_type {
EventType::KeyPress(key) => {
// 在 should_restrict 里面编写你的拦截逻辑, 判断是否拦截
if should_restrict(key) {
None
} else {
Some(ev)
}
}
_ => Some(ev),
});
修改注册表拦截一些特殊按键
rdev
可以拦截绝大部分 windows 的按键,但是有一些键很特殊,是不能通过写 hook 来拦截的,比如这三个键
win + l
锁屏win + g
弹出 windows gamectrl + alt + delete
CAD
上面三个键只能通过修改用户的注册表来实现拦截
修改注册表需要 windows 的管理员权限,请保证你的 electron app 具有管理员权限
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
示例如下
typescript
// 禁用 win + l 锁屏
import regeditOrigin from "regedit"
const regedit = regeditOrigin.promisified
async function main() {
const winLRegisterPath =
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"
try {
await regedit.createKey([winLRegisterPath])
} catch (error) {
console.error(error)
}
await regedit.putValue({
[winLRegisterPath]: {
DisableLockWorkstation: {
type: "REG_DWORD",
value: 1,
},
},
})
}
regedit 在调用的时候需要将 HKEY_CURRENT_USER 需要改成 HKCU
如法炮制即可禁用剩余的 win
快捷键
合并 mac 和 win 的逻辑
在 rust
里面可以使用条件编译, 有 cpp
经验的应该知道这个
rust
fn prevent_keyboard_event() {
#[cfg(target_os = "macos")]
{
// mac os 相关代码
}
#[cfg(target_os = "windows")]
{
// windows 相关代码
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
// 其他平台
}
}
使用 napi-rs 打包, 用 github actions
编译成 npm 库
(本质上是 .node
文件), 便可在 electron
中直接调用, 实现霸屏功能的一部分.
欢迎各位在评论区友好讨论
真不好搞 😭