macOS用户输入事件NSEvent
macOS用户输入事件NSEvent

macOS用户输入事件NSEvent

NSEvent 是 macOS AppKit 框架中用于表示用户输入事件(例如鼠标、键盘、触控板等)的核心类。几乎所有用户交互行为都会被封装为一个 NSEvent 对象,并由系统传递给 app 的响应链。

NSEvent 代表系统级事件对象,可以:

1、监听键盘按键(按下、松开)。

2、监听鼠标移动、点击、滚动。

3、监听触控板手势(两指滚动、捏合缩放等)。

4、响应系统事件(如 app 激活、窗口焦点变化)。

常用属性

1、通用属性(所有事件都有)

1、type:NSEvent.EventType类型,事件类型,如 .keyDown。

2、timestamp:TimeInterval类型,事件发生的时间戳。

3、locationInWindow:NSPoint类型,鼠标/触摸事件相对窗口的位置。

4、window / windowNumber:NSWindow?类型,触发事件的窗口。

5、modifierFlags:NSEvent.ModifierFlags类型,当前按下的修饰键(如 ⌘ ⌥ ⇧)。

可组合使用:

event.modifierFlags.contains(.command)
event.modifierFlags.contains(.shift)
event.modifierFlags.intersection([.command, .option])

全部修饰符包括:.command、.option、.shift、.control、.capsLock和.function(Fn)。

6、context:NSGraphicsContext?类型,图形上下文(通常用不到)。

7、eventRef:Unmanaged<CGEvent>?类型,原始低层事件(用于精细处理)。

8、cgEvent:CGEvent?类型,Core Graphics 层级事件对象(更底层,macOS 10.6+)。

9、appKitDefined / .systemDefined / .applicationDefined / .periodic:系统内部定义或应用自定义事件。

2、键盘事件(keyDown / keyUp)

1、keyCode:UInt16类型,原始硬件键码(设备级别)。

2、characters:String?类型,按下的键所产生的字符,受修饰键影响(如 shift)。

3、charactersIgnoringModifiers:String?类型,按下的键对应的字符,忽略修饰键(除 Shift)。

if event.modifierFlags.contains(.command),event.charactersIgnoringModifiers == "v" { }

这表示监听Command + v快捷键。

4、isARepeat / isKeyRepeat:Bool类型,是否是按住键后的重复触发。

5、unicodeScalarValues:[UnicodeScalar]?类型,更底层的字符编码值(macOS 13+)。

6、hasMarkedText:Bool类型,是否为输入法中未完成的输入(如拼音)。

7、markedRange:NSRange类型,当前输入法标记文字的范围。

8、selectedRange:NSRange类型,当前选中的文字范围。

3、鼠标事件(mouseDown / mouseDragged / mouseUp)

1、buttonNumber:Int类型,鼠标按下的是哪个按钮(0 左键,1 右键,2 中键)。

2、clickCount:Int类型,连续点击次数(1 单击,2 双击)。

3、pressure:Float类型,压力值(如 Force Touch,0~1)。

4、trackingNumber:Int类型,用于追踪区域鼠标。

5、eventNumber:Int类型,系统内部的事件序号。

6、mouseLocation:NSPoint类型,当前鼠标在屏幕坐标下的位置。

7、trackingArea:NSTrackingArea?类型,所属的跟踪区域(通常用于 mouseEntered / mouseExited)。

8、associatedEventsMask:NSEvent.EventTypeMask类型,鼠标拖拽中可以附带哪些事件类型。

9、buttonMask:Int类型,当前按下的所有鼠标按钮的组合(macOS 10.10+)。

10、cursorUpdate:鼠标光标区域变更(例如进入链接)。

4、滚轮事件(scrollWheel)

1、deltaX / deltaY / deltaZ:CGFloat类型,滚动的位移(横向/纵向/深度)。

2、hasPreciseScrollingDeltas:Bool类型,是否是精确滚动(如触控板)。

3、scrollingDeltaX/Y:CGFloat类型,实际滚动增量。

4、isDirectionInvertedFromDevice:Bool类型,滚动方向是否反转(与系统设置有关)。

5、触控板、触摸事件

这些只有在处理 gesture 或 touch 事件时才有意义:

1、touches(matching:in:):Set<NSTouch>类型,获取与事件关联的触摸信息。

2、allTouches():Set<NSTouch>类型,获取所有触摸对象。

3、magnification:CGFloat类型,捏合手势缩放值(用于 magnify 事件)。

4、rotation:CGFloat类型,旋转手势的角度(用于 rotate 事件)。

5、phase:NSEvent.Phase类型,当前触控阶段(began、moved、ended)。

6、momentumPhase:NSEvent.Phase类型,动量阶段,如滚动结束的惯性部分。

7、.beginGesture / .endGesture:手势开始/结束。

8、smartMagnify:智能缩放手势(双指双击)。

9、.touchesBegan / .touchesMoved / .touchesEnded / .touchesCancelled:多点触控事件(Trackpad)。

6、Apple Pencil / 手写板支持(较少用)

1、tilt:NSEvent.Tilt类型,输入设备的倾斜角度(iPad 手写板相关)。

2、vendorDefined:Data类型,自定义厂商事件数据。

3、.tabletPoint / .tabletProximity:手写板输入,不过多介绍。

7、Game Controller / 特殊设备输入(很少用)

1、subtype:Int类型,事件子类型(例如遥控器输入)。

2、data1 / data2:Int类型,原始设备数据字段。

类级别属性

NSEvent 作为一个类,提供了若干类级别(Type-Level)的属性与方法,用于访问系统当前输入状态、注册全局事件监听器、获取鼠标状态等。

1、鼠标位置与状态

1、mouseLocation:NSPoint类型,当前鼠标在全局屏幕坐标系中的位置(左下为原点)。

print("\(NSEvent.mouseLocation)")   // (917.01171875, 875.40625)

2、pressedMouseButtons:Int类型,当前按下的鼠标按钮组合(按位编码)。

2、当前按键状态

1、modifierFlags:NSEvent.ModifierFlags类型,当前所有修饰键(⌘ ⌥ ⇧ ⌃等)的状态。

let flags = NSEvent.modifierFlags
if flags.contains(.command) && flags.contains(.option) {
    print("⌘ 和 ⌥ 被按下了")
}

这里是读取实时的物理修饰符,⌘ 和 ⌥ 被同时按下后,执行该代码才会触发if语句。

2、keyRepeatDelay:TimeInterval类型,键盘按键初次长按的延迟时间。

3、keyRepeatInterval:TimeInterval类型,按键持续按下时重复的时间间隔。

3、全局事件监控(全局监听鼠标、键盘、滚轮等)

1、addGlobalMonitorForEvents(matching:handler:):Any?类型,监听所有应用中的事件(无权阻止)要在前台 App 中使用。

NSEvent.addGlobalMonitorForEvents(matching: .scrollWheel) { event in
    print("滚轮 deltaY: \(event.scrollingDeltaY)")
}

2、addLocalMonitorForEvents(matching:handler:):Any?类型,监听当前应用中的事件(可修改、阻止)。

3、removeMonitor(_:):Void类型,移除上面两个 monitor 中返回的对象。

4、鼠标按钮工具方法

1、doubleClickInterval:TimeInterval类型,系统双击的最大时间间隔。

2、mouseCoalescingEnabled:Bool类型,是否启用鼠标事件合并(减少事件数量)。

3、setMouseCoalescingEnabled(_:):Void类型,设置上面这个开关。

5、系统输入管理器相关(较少使用)

1、isMouseCoalescingEnabled:Bool类型,是否合并鼠标事件(macOS 13+ 改名为此)。

2、startPeriodicEvents(afterDelay:withPeriod:):Void类型,启动周期性事件(发送 .periodic)。

3、stopPeriodicEvents():Void类型,停止周期性事件。

使用场景

1、响应事件(事件处理)

继承 NSView 或 NSWindow 时处理输入事件,比如在自定义视图、窗口或控制器中响应鼠标、键盘等:

override func mouseDown(with event: NSEvent) {
    if event.clickCount == 2 {
        print("双击了鼠标")
    }
    print("点击位置:\(event.locationInWindow)")
}

override func keyDown(with event: NSEvent) {
    print("按下键盘:\(event.characters ?? "")")
}

常用的事件处理方法:

1、mouseDown(with:):鼠标按下。

2、mouseDragged(with:):鼠标拖动。

3、mouseUp(with:):鼠标释放。

4、scrollWheel(with:):鼠标滚动。

5、keyDown(with:):键盘按下。

6、keyUp(with:):键盘松开。

7、scrollWheel(with:):鼠标滚轮。

必须放在 NSResponder 子类(如 NSView、NSWindow)中,属于响应链一部分(用户点击后自动传递),可以覆盖并拦截(不调用 super 就不会继续传递)。

2、全局/本地监听事件(事件监控)

适合用于状态栏应用、后台监控工具、快捷键等。

示例,监听当前应用的键盘按键:

let monitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { event in
    print("本地监听到按键:\(event.characters ?? "")")
    return event // 或 return nil 来阻止事件
}

可以拦截事件(返回 nil 就不会继续传递),运行在当前 app 内部。

示例,监听所有应用的键盘按键:

let monitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]) { event in
    print("全局监听键盘:\(event)")
}

无法拦截(返回值无效),可监听所有 app 的事件(前提是开启辅助功能权限)。适合快捷键监听器、截图工具、辅助工具等。

事件方法和NSEvent属性

mouseDown(with:)等方法可以响应鼠标点击行为,这些方法都接收一个NSEvent实例。

override func mouseDown(with event: NSEvent) {		// mouseDown()是事件方法
    if event.clickCount == 2 {	// clickCount是NSEvent属性
        print("双击了鼠标")
    }
    print("点击位置:\(event.locationInWindow)")
}

NSEvent的属性指的是事件对象本身所包含的数据,比如鼠标点击的位置等信息。

事件传递机制

NSEvent 被系统发送到视图树上的响应链中,由最先响应的 NSResponder(比如 NSView, NSWindow, NSViewController)依次处理。

比如:

1、用户点击窗口 → 系统创建 NSEvent。

2、发送到最前面的窗口(NSWindow)。

3、传递到窗口的 contentView。

4、传到具体控件(如 NSButton)。

作用域与行为范围

NSEvent.addLocalMonitorForEvents(…) 是应用级的事件监听器,

无论在 App 的哪里注册(子视图中、控制器中、单例中),它都能监听整个 App 内部的键盘事件,前提是这个 App 正在“激活并响应键盘”。

例如,在子视图中注册:

struct MyView: View {
    init() {
        NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
            print("收到事件")
            return event
        }
    }

    var body: some View {
        Text("Hello")
    }
}

这段代码也能监听整个 App 内的键盘事件,不是只监听 MyView。

但是,addLocalMonitorForEvents只有在当前App为前台、主窗口时,才会响应。如果App在后台,则不会监听。

如果想要在后台也实现监听功能,就需要addGlobalMonitorForEvents,但它不允许拦截,只能监听(而且需要辅助权限)。

addLocalMonitorForEvents,作用于整个App,可以拦截事件,不需要权限。

addGlobalMonitorForEvents作用于所有App,不可以拦截事件,需要“辅助功能”权限。

因此在使用addLocalMonitorForEvents时,通常建议封装到单例中统一管理,避免多处监听,如果在多个视图中重复注册,就可能导致监听器混乱或重复触发。

若要监听全局(系统级)按键行为,请使用 addGlobalMonitorForEvents + 请求用户启用“辅助功能”。

总结

在macOS(AppKit)开发中,监听键盘和鼠标、处理触控板手势、实现快捷键等功能,需要使用NSEvent。

当我们在日常使用的过程中,以截图工具为例,通常会要求我们授权“辅助功能”,这里就是利用NSEvent的addGlobalMonitorForEvents方法,监听键盘、鼠标和滚轮等信息(即使应用处于后台)。

但因为它监听了我们的键盘等信息,才能在其他应用打开时,实现截图效果。因此,在授权“辅助功能”时应该考虑到这一点,这涉及隐私问题。

扩展知识

NSEvent.EventType种类

NSEvent.EventType 是枚举类型,代表事件种类:

1、.keyDown / .keyUp:按键按下/抬起

2、.mouseDown / .mouseUp / .mouseMoved:鼠标点击、松开、移动

3、.rightMouseDown / . rightMouseUp:右键按下/抬起。

4、.flagsChanged:修饰键(Shift/Command)变化。

5、.scrollWheel:鼠标滚轮或触控板滚动。

6、gesture / .magnify / .swipe:手势、缩放、滑动。

7、.otherMouseDown:其他鼠标键(如中键)。

在键盘监听这种场景下,Combine 是否不可替代?

在“SwiftUI使用NSEvent监听剪贴板”示例中,涉及到复杂的Combine,也许会想要通过变量的修改来提到Combine:

@Published var didPressPaste: Bool = false

在SwiftUI中监听这一变量:

.onChange(of: KeyboardMonitor.shared.didPressPaste) { ... }

这里涉及到使用的场景:

1、当我们的状态变化是持久的,可以使用SwiftUI监听@Published变量。

2、如果是事件流(可多次触发),应该使用Combine的PassthroughSubject,因为它专门用于“瞬时”事件(不是状态)。

键盘 ⌘V 是一种”事件”而不是”状态”,所以 Combine 是最合适的方式。

这里的区别在于,@Published会保留数据,并且当数据修改时,如果数值是重复的,那么就不会触发通知,所以不适合“粘贴事件”。

Combine的PassthroughSubject属于离散的瞬时事件,它不会保留数据,每次触发(即使是重复值)都会通过send()发送通知,适合“粘贴事件”。

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注