一个NSResponder实例对象有三个组件:事件消息(鼠标,键盘,触控板等产生的),动作消息(action message: 比如NSButton 执行target 的action 方法,就属于一种action消息),和响应链条
一个应用(NSApplication对象)维护着一组窗口(NSWindow)列表,这些窗口都属于这个App,每个窗口对象又维护着一组继承自NSView的对象,这些NSView对象通常用来绘制交互界面以及处理响应事件.
一个窗口对象(NSWindow)处理窗口级别的事件(window-level events)以及将其他事件传递给窗口中的视图对象,同时一个NSWindow还允许通过它的delegate实现自定义窗口的行为方式.
我们这里说的事件,是指用户通过连接到macOS系统中的鼠标,键盘或者触控板,手写笔等硬件设备的具体操作(比如按下鼠标的按键).
每个应用都有一种明确的机制用来确保从操作系统的 窗口服务 中获取事件( Event ).在 Cocoa Application 中,这种机制叫做 runloop (一个NSRunLoop对象,它允许进程接收 窗口服务 的各种来源).默认情况下,OSX中每个线程都有自己的 runloop .NSApplication 主线程的 runloop 称为 main runloop ,主事件循环的一个显著特点是它由 NSApplication 对象创建的事件输入源(也就是其他对象,通常是操作系统的 窗口服务 ,可以向它添加事件源).
为了能从 窗口服务 接收事件和对接收到的事件进行处理,runloop通常包含这两个部分:端口( Mach port )和事件队列( event queue )
从另一种意义上讲,应用程序是被事件( event )驱动的:
在主事件循环中( main event runloop ),应用程序对象(NSApp)会不断的从事件队列中(event queue)获取最前面的事件,然后将它转换为NSEvent 对象后,派发到最终目标.
由此可见,在事件派发的过程中,会根据事件种类(AppKit中定义的 NSAppKitDefined 类型)的不同而进行不同的派发选择.有些事件只能由NSWindow或者NSApplication自身来处理,比如应用的隐藏/显示/激活状态/失去激活状态等.
前面已经提到过,一个 NSWindow 对象使用sendEvent:方法将鼠标事件派发给用户操作的视图(NSView)对象.那么 NSWindow 是怎样识别是哪个NSView在被用户操作呢?是通过调用 NSView 的 hitTest: 方法,根据这个方法的返回值(通常是显示在最顶层的View)来确定.
NSWindow 对象是将事件以一个与鼠标相关的 NSResponder 明确消息方式发送视图(NSView),比如 mouseDown: , mouseDragged: ,或者 rightMouseUp: ,如果是鼠标按下事件, NSWindow 还会询问 NSView 是否希望成为第一响应者,以便接收 键盘 和 action 消息.
一个 NSView 对象可以接收三种类型的鼠标事件: 鼠标点击 , 鼠标拖拽 和 鼠标移动 .
鼠标点击事件可以根据点击方向( 按下或抬起 )和鼠标按钮( 左键,右键,或其他 )被进一步的细化分类,这些定义在了 NSEventType 和 NSResponder 中.
鼠标拖动事件 和 鼠标抬起事件 通常都会被发送给之前 鼠标按下的那个视图(NSView)对象 .
鼠标移动事件通常会派发到 第一响应者 .
当用户在一个视图控件上点击鼠标按钮后,如果包含这个视图的NSWindow不是key Window,那么这个NSWindow将会变成key Window,并且丢弃本次的鼠标事件;也就是说如果你用鼠标点击了一个不是key Window窗口中的一个(NSButton)按钮时,这个点击动作仅仅是将这个窗口( NSWindow )对象变成 key Window 而已,你还需要使用鼠标 再次点击 这个按钮,此时这个按钮才会接收到 鼠标点击 的事件. 如果你要避免这种情况,可以通过重写NSView的acceptsFirstMouse: 方法,并返回YES
NSView 通常会自动接收 鼠标点击 和 鼠标拖拽 事件,而不会主动接收 鼠标移动 事件.因为 鼠标移动 事件发生的太过频繁, 很容易阻塞事件队列 ,所以默认情况下 NSView 不响应 鼠标移动 事件.如果一个NSView需要处理 鼠标移动 事件,那么需要向它的窗口对象(NSWindow)明确的声明一下,也就是调用NSWindow的 setAcceptsMouseMovedEvents:方法
响应键盘输入是事件派发中最复杂的部分之一.Cocoa 应用程序会遍历每一个键盘事件来确定它属于那种类型然后以及如何处理.先来看一下苹果官方给出的一个键盘事件可能的传递传递路径:
下面我们来解释一下:
关于控制键的更详细内容,有兴趣的同学可以通过这个链接 Handling Key Events 查看苹果官方的文档
在 应用程序 处理键盘事件时,如果这个事件不是 快捷键(Key equivalents) 或者 控制键Keyboard interface control ,那么 应用程序 会将事件通过 sendEvent: 方法发送给 kew window ,然后窗口(key window)对象会调用第一响应者的 keyDown: 方法,将事件传递到整个响应链条中.
关于键盘事件的派发与处理细节,大家可以查看苹果官方文档 Handling Key Events
在应用程序中,我们可以使用 NSTrackingArea 类添加一个监控区域,这些事件 NSWindow 对象会直接派发到拥有这个区域的指定对象(通常发送 mouseEntered:和 mouseExited:消息).
应用程序(NSApplication)生成的周期性事件(NSPeriodic)通常不会使用 sendEvent: 派发,它们是通过某个NSObject对象注册后(通过调用nextEventMatchingMask:untilDate:inMode:dequeue: 方法)才会得到处理.具体的详细内容,可以参考 Other Types of Events