Mac 鼠标/键盘事件的监听和模拟
参考:
《macOS AppKit 的事件响应简介》
《Mac OSX 鼠标键盘事件的监听和模拟》
事件分发机制:
在 macOS 系统中鼠标、键盘和触摸板的活动事件都会产生底层的系统事件,首先传递到 IOKit 框架处理后存储到队列中,通知 Window Server 服务层处理。Window Server 存储到 FIFO 优先队列中,然后逐一转发到当前活动窗口或者能响应这个事件的应用程序去处理。
在 macOS 或者 iOS 程序中,都会有一个 Main Run Loop 的线程,RunLoop 循环中会遍历 event 消息队列,逐一分发这些事件到应用中合适的对象去处理。具体来说就是调用 NSApp
的 sendEvent:
方法发送消息到NSWindow
,NSWindow
再分发到 NSView
视图对象,由其鼠标或键盘事件响应方法去处理。
事件响应链:
响应者链是 Application Kit 事件处理架构的中心机制,由一系列链接在一起的响应者对象组成,事件或者动作消息可以沿着这些对象进行传递。消息沿着响应者链向上、向更高级别的对象传递,直到最终被处理(如果最终还是没有被处理,就会被抛弃)。
事件响应者 Responders 类为核心应用程序架构的三个主要模式或机制定义了一个接口:
- 它声明了一些处理事件消息(也就是源自用户事件的消息,比如鼠标点击或按键按下这样的事件)的方法。
- 它声明了数十个处理动作消息的方法,它们和标准的键绑定(比如那些在文本内部移动插入点的绑定)密切相关。动作消息会被派发到目标对象;如果目标没有被指定,应用程序会负责检索合适的响应者。
- 它定义了一套在应用程序中指派和管理响应者的方法。这些响应者组成了我们所知道的响应者链,即一系列响应者,事件或动作消息在它们之间传递,直到找到能够对它们进行处理的对象。
从层级上看离观察者最近的视图优先响应事件,通过 view 的 hitTest 方法检测,满足 hitTest 方法的的子视图优先响应事件。
NSApplication
, NSWindow
, NSDrawer
, NSWindowController
, NSView
以及继承于 NSView
的所有控件对象都直接或间接继承了 Responders 类,所以这些类都能处理鼠标和键盘事件。
相关的类
NSResponder:https://developer.apple.com/documentation/appkit/nsresponder
NSEvent:https://developer.apple.com/documentation/appkit/nsevent
NSEventType:https://developer.apple.com/documentation/appkit/nseventtype
NSEventModifierFlags:https://developer.apple.com/documentation/appkit/nseventmodifierflags/
事件的监听方法
《Mac OSX 鼠标键盘事件的监听和模拟》中提到:鼠标/键盘事件的监听有多种方法,第一种方法是重写事件响应者 Responders 对应的方法来获取对应的事件;第二是通过重写 NSWindow 的 sendEvent: 方法; 第三是通过的 NSEvent 提供静态方法来监听对应的事件~
没有逐一去试验,如下键盘事件/鼠标事件只是各用一种方式实现了相应监听!
- [A].键盘事件的监听——通过的
NSEvent
提供静态方法来监听对应的事件!
NSEvent
提供的静态方法可以用监听整个系统的事件或者当前应用程序内的事件。
+ (nullable id)addGlobalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(void (^)(NSEvent*))block`
+ (nullable id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent* __nullable (^)(NSEvent*))block
+ (void)removeMonitor:(id)eventMonitor
Swift实现代码:(开启对键盘的监听,并书写其响应方法)
//开启对键盘的监听 NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.flagsChanged) { self.flagsChanged(with: $0) return $0 } NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyDown) { self.keyDown(with: $0) return $0 } NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyUp) { self.keyUp(with: $0) return $0 }
键盘事件的响应方法:
//MARK:KeyBoard键盘的响应 override func keyUp(with event: NSEvent) { //键盘抬起:(含)普通按键Key——可一直输入的Key按键 } override func keyDown(with event: NSEvent) {//键盘按下:(含)普通按键Key——可一直输入的Key按键 } override func flagsChanged(with event: NSEvent) {//按键变化:(仅有)特殊的功能控制键Key——shift、control、option、option及相互组合 }
- [B].鼠标事件的监听——通过使用重写 Responders 的方法来监听鼠标事件:
鼠标的事件类型:
1.左/右键的按下与抬起事件
2.左键的双击(或者多击事件)——clickCount
属性
3.鼠标移动事件
4.左键或者右键的拖拽事件
5.鼠标的滚动事件
使用如下重写 Responders 的方法来监听鼠标事件:
- (void)mouseDown:(NSEvent *)event;
- (void)rightMouseDown:(NSEvent *)event;
- (void)mouseUp:(NSEvent *)event;
- (void)rightMouseUp:(NSEvent *)event;
- (void)mouseMoved:(NSEvent *)event;
- (void)mouseDragged:(NSEvent *)event;
- (void)rightMouseDragged:(NSEvent *)event;
- (void)scrollWheel:(NSEvent *)event;
Swift实现代码:(直接重写 其响应方法)
//MARK:Mouse鼠标的响应 override func mouseDown(with event: NSEvent) { } override func rightMouseDown(with event: NSEvent) { } override func mouseUp(with event: NSEvent) { } override func rightMouseUp(with event: NSEvent) { } override func mouseMoved(with event: NSEvent) { } override func mouseDragged(with event: NSEvent) { } override func rightMouseDragged(with event: NSEvent) { } override func scrollWheel(with event: NSEvent) { }
使用例子🌰:(在'ViewController.swift'文件中)
import Cocoa
class ViewController: NSViewController,NSWindowDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//开启对键盘的监听
NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.flagsChanged) {
self.flagsChanged(with: $0)
return $0
}
NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyDown) {
self.keyDown(with: $0)
return $0
}
NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyUp) {
self.keyUp(with: $0)
return $0
}
}
//MARK:KeyBoard键盘事件的响应
override func keyUp(with event: NSEvent) { //键盘抬起:(含)普通按键Key——可一直输入的Key按键
let keyCode = event .keyCode //类型:CUnsignedShort即UInt16
print("keyUp-> keyCode:\(keyCode) event.characters:\(event.characters as Any)")
}
override func keyDown(with event: NSEvent) {//键盘按下:(含)普通按键Key——可一直输入的Key按键
let keyCode = event .keyCode //类型:CUnsignedShort即UInt16
print("keyCode:\(keyCode) event.characters:\(event.characters as Any)")
//根据 对应的`event .keyCode`数值和`event.characters`字符串,来进行相应操作
if keyCode == 53 {//点击了'Esc'按键
print("press 'Esc' key")
}
//处理“特殊的功能控制按键Key+普通按键Key”按键组合
switch event.modifierFlags.intersection(NSEvent.ModifierFlags.deviceIndependentFlagsMask) {
case [.command] where event.characters == "l", [.command, .shift] where event.characters == "l":
print("command-l or command-shift-l")
default:
break
}
}
override func flagsChanged(with event: NSEvent) {//按键变化:(仅有)特殊的功能控制按键Key——shift、control、option、option及相互组合 NSEventModifierFlags
print("flagsChanged->", event.modifierFlags.intersection(NSEvent.ModifierFlags.deviceIndependentFlagsMask))
switch event.modifierFlags.intersection(NSEvent.ModifierFlags.deviceIndependentFlagsMask) {
case [.shift]:
print("shift key is pressed")
case [.control]:
print("control key is pressed")
case [.command]:
print("command key is pressed")
case [.option]:
print("option key is pressed")
case [.control, .shift]:
print("control-shift keys are pressed")
case [.control, .command]:
print("control-command keys are pressed")
case [.control, .option]:
print("control-option keys are pressed")
case [.command, .shift]:
print("command-shift keys are pressed")
case [.option, .shift]:
print("option-shift keys are pressed")
case [.option, .command]:
print("option-command keys are pressed")
case [.shift, .control, .command]:
print("shift-control-command keys are pressed")
case [.shift, .control, .option]:
print("shift-control-option keys are pressed")
case [.shift, .command, .option]:
print("shift-command-option keys are pressed")
case [.control, .option, .command]:
print("control-option-command keys are pressed")
case [.shift, .control, .option, .command]:
print("shift-control-option-command keys are pressed")
default://抬手时也会响应——NSEventModifierFlags(rawValue: 0)
break //print("no modifier keys are pressed")//❌
}
}
//MARK:Mouse鼠标事件的响应
override func mouseDown(with event: NSEvent) {
//event.type——判断鼠标的操作、event.locationInWindow——获取鼠标的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue为1
}
override func mouseUp(with event: NSEvent) {
//event.type——判断鼠标的操作、event.locationInWindow——获取鼠标的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue为2
}
override func rightMouseDown(with event: NSEvent) {
//event.type——判断鼠标的操作、event.locationInWindow——获取鼠标的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue为3
}
override func rightMouseUp(with event: NSEvent) {
//event.type——判断鼠标的操作、event.locationInWindow——获取鼠标的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue为4
}
override func mouseMoved(with event: NSEvent) {
//event.type——判断鼠标的操作、event.locationInWindow——获取鼠标的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue为5
}
override func mouseDragged(with event: NSEvent) {
//event.type——判断鼠标的操作、event.locationInWindow——获取鼠标的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue为6
}
override func rightMouseDragged(with event: NSEvent) {
//event.type——判断鼠标的操作、event.locationInWindow——获取鼠标的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue为7
}
override func scrollWheel(with event: NSEvent) {
//event.type——判断鼠标的操作、event.locationInWindow——获取鼠标的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue为22
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
键盘事件的响应:其中keyUp
方法和keyDown
方法——点击时只要含有普通按键就会响应、flagsChanged
方法——只响应 特殊的功能控制按键!
鼠标事件的响应:在鼠标事件的方法中,通过event.type
——判断鼠标进行的相应操作、event.locationInWindow
——获取鼠标的位置~
Tips:在代码中引入“
Carbon.HIToolbox
”(OC中:“Carbon/HIToolbox/Events.h
”):import Carbon.HIToolbox//OC中:Carbon/HIToolbox/Events.h
就可以‘kVK_’的对应值更直观来进行判断:
可将
if keyCode == 53 {//点击了'Esc'按键 print("press 'Esc' key") }
替换为
if keyCode == kVK_Escape {//点击了'Esc'按键 print("press 'Esc' key") }
来进行判断~
Tips:要响应鼠标的
mouseEntered
、mouseExited
、mouseMoved
回调方法,需要为对应的NSView实例添加上NSTrackingArea(监视区域)~
模拟事件 (C语言方式)
1.模拟鼠标事件:
void PostMouseEvent(CGMouseButton button, CGEventType type, const CGPoint &point, int64_t clickCount)
{
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate);
CGEventRef theEvent = CGEventCreateMouseEvent(source, type, point, button);
CGEventSetIntegerValueField(theEvent, kCGMouseEventClickState, clickCount);
CGEventSetType(theEvent, type);
CGEventPost(kCGHIDEventTap, theEvent);
CFRelease(theEvent);
CFRelease(source);
}
左键单击模拟:
PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 1);
左键双击模拟:
PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 2); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 2);
拖拽事件:
如果是拖拽事件,例如左键拖拽事件,则需要先发送左键的kCGEventLeftMouseDown
事件,然后连续发送kCGEventLeftMouseDragged
事件,再发送kCGEventLeftMouseUp
事件,代码如下:PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDragged, CGPointZero, 1); ... PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDragged, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 1);
模拟其他鼠标事件,将枚举值修改一下即可。
void PostScrollWheelEvent(int32_t scrollingDeltaX, int32_t scrollingDeltaY)
{
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate);
CGEventRef theEvent = CGEventCreateScrollWheelEvent(source, kCGScrollEventUnitPixel, 2, scrollingDeltaY, scrollingDeltaX);
CGEventPost(kCGHIDEventTap, theEvent);
CFRelease(theEvent);
CFRelease(source);
}
鼠标滚轮事件只要传入水平和垂直方向的偏移即可实现。
void PostKeyboardEvent(CGKeyCode virtualKey, bool keyDown, CGEventFlags flags)
{
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate);
CGEventRef push = CGEventCreateKeyboardEvent(source, virtualKey, keyDown);
CGEventSetFlags(push, flags);
CGEventPost(kCGHIDEventTap, push);
CFRelease(push);
CFRelease(source);
}
键盘事件的模拟需要注意的就是 CGEventFlags flags
参数,该参数用来模拟组合键的实现,类型定义如下:
kCGEventFlagMaskAlphaShift
:大小写锁定键是否处于开启状态
kCGEventFlagMaskShift
:Shift 键是否按下
kCGEventFlagMaskControl
:Control 键是否按下
kCGEventFlagMaskAlternate
:Alt 键是否按下,对应 Mac 键盘的 option 键
kCGEventFlagMaskCommand
:Command 键是否按下,对应 Windows 的 WIN 键
kCGEventFlagMaskHelp
:Help 键
kCGEventFlagMaskSecondaryFn
:Fn 键
kCGEventFlagMaskNumericPad
:数字键盘
kCGEventFlagMaskNonCoalesced
:没有任何键按下
如果有多个控制键同时按下,则使用位运算的或 | 加上对应的键值即可。例如模拟 Command
+ Control
+ S
:
PostKeyboardEvent(kVK_ANSI_S, true, kCGEventFlagMaskCommand | kCGEventFlagMaskControl)
PostKeyboardEvent(kVK_ANSI_S, false, kCGEventFlagMaskNonCoalesced)
注意:大小写锁定键,无法通过
kVK_CapsLock
按键的按下和抬起事件来模拟大小键的锁定,同时按键上的 LED 灯也是不会有变化的。