Mac 鼠标/键盘事件的监听和模拟


参考:
macOS AppKit 的事件响应简介
Mac OSX 鼠标键盘事件的监听和模拟


事件分发机制:

在 macOS 系统中鼠标键盘触摸板的活动事件都会产生底层的系统事件,首先传递到 IOKit 框架处理后存储到队列中,通知 Window Server 服务层处理。Window Server 存储到 FIFO 优先队列中,然后逐一转发到当前活动窗口或者能响应这个事件的应用程序去处理。
在 macOS 或者 iOS 程序中,都会有一个 Main Run Loop 的线程,RunLoop 循环中会遍历 event 消息队列,逐一分发这些事件到应用中合适的对象去处理。具体来说就是调用 NSAppsendEvent: 方法发送消息到NSWindowNSWindow 再分发到 NSView 视图对象,由其鼠标或键盘事件响应方法去处理。

EventDispatch
Apple event


事件响应链:

响应者链是 Application Kit 事件处理架构的中心机制,由一系列链接在一起的响应者对象组成,事件或者动作消息可以沿着这些对象进行传递。消息沿着响应者链向上、向更高级别的对象传递,直到最终被处理(如果最终还是没有被处理,就会被抛弃)。

事件响应者 Responders 类为核心应用程序架构的三个主要模式或机制定义了一个接口:

  • 它声明了一些处理事件消息(也就是源自用户事件的消息,比如鼠标点击或按键按下这样的事件)的方法
  • 它声明了数十个处理动作消息的方法,它们和标准的键绑定(比如那些在文本内部移动插入点的绑定)密切相关。动作消息会被派发到目标对象;如果目标没有被指定,应用程序会负责检索合适的响应者。
  • 它定义了一套在应用程序中指派和管理响应者的方法。这些响应者组成了我们所知道的响应者链,即一系列响应者,事件或动作消息在它们之间传递,直到找到能够对它们进行处理的对象。

从层级上看离观察者最近的视图优先响应事件,通过 view 的 hitTest 方法检测,满足 hitTest 方法的的子视图优先响应事件。

NSApplication, NSWindow, NSDrawer, NSWindowController, NSView 以及继承于 NSView 的所有控件对象都直接或间接继承了 Responders 类,所以这些类都能处理鼠标和键盘事件。


相关的类

NSResponderhttps://developer.apple.com/documentation/appkit/nsresponder

NSEventhttps://developer.apple.com/documentation/appkit/nsevent
NSEventTypehttps://developer.apple.com/documentation/appkit/nseventtype
NSEventModifierFlagshttps://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")
}

来进行判断~


各种‘kVK_’的对应值如下:

Tips:要响应鼠标的mouseEnteredmouseExitedmouseMoved回调方法,需要为对应NSView实例添加上NSTrackingArea(监视区域)~

请参考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);

模拟其他鼠标事件,将枚举值修改一下即可。



2.模拟鼠标滚动事件

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);
}

鼠标滚轮事件只要传入水平和垂直方向的偏移即可实现。



3.模拟键盘事件

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 灯也是不会有变化的。










goyohol's essay

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,999评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,102评论 1 302
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,709评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,439评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,846评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,881评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,062评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,783评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,517评论 1 248
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,762评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,241评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,236评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,145评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,941评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,965评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,802评论 2 275

推荐阅读更多精彩内容