原文链接:读Cocoa事件处理机制有感(Event Architecture)
每个应用程序都有一种接收(Window Server)窗口服务器事件的机制。对于一个Cocoa应用程序,这种机制叫主事件循环(the main event loop)。 进程用NSRunLoop对象接收来自各种源的输入。默认情况下,在OS X的每个线程都有它自己的运行循环,而一个Cocoa应用程序主线程的运行循环叫做主事件循环。主事件循环和非实践循环的区别在于它的输入源(Event source)主要接收来自操作系统的事件。输入源(Event source)是由一个从窗口服务器接收事件的端口和一个存储待处理事件FIFO队列组成。如下图
图1-2主事件循环,事件源
[图片上传失败...(image-7ffbb0-1588133958235)]
WindowServer 从I/O Kit设备驱动程序接收事件,并派发到恰当的进程中,而进程则在运行循环中接收来自事件源事件,并将他们放到队列中。
Cocoa应用程序是事件驱动的:它会从队列中的事件,将它分派给适当的对象,并在一件事件处理完后,取下一个事件。有一些例外(如模态事件循环)应用程序继续留在这个模式直到用户退出。下面的部分讲事件调度:介绍应用程序如何获取和分发事件。
应用程序接收事件类型多种多样。 应用程序还可以应对苹果事件(Apple Events),高级进程事件交互如启动Finder和启动服务 。例如当用户双击应用程序图标打开应用程序或双击一个文件,打开文件,一个苹果事件发送到目标应用程序。应用程序会从队列中提取苹果事件,但不会把他们转换成NSEevet对象。而是由程序来直接办理。当应用程序启动时,它会自动注册一些处理事件处理器来处理这些Apple Event。
事件分派:
事件分派总的来就应用程序对象(NSApp)从队列中获取一个事件,并把这个事件转换为NSEvent,再把NSEvent分派到最终的目的地。
上文已经把window server窗口服务器事件存储在事件队列中,那事件分派就讲两件事情,第一队列的工作流程就是怎么把事件从事件队列中取出来,第二从队列中取出的事件是如何分派到对应的View。
首先说事件队列的工作流程:应用程序对象(NSApp)调用nextEventMatchingMask:untilDate:inMode:dequeue:方法在封闭的循环中获取事件。当没有事件的时候,这个循环就阻塞。只有当有新的事件要处理的时,循环才resume。
事件分派:应用程序对象(NSApp)首先把转换后的NSEvent通过sendEvent:方法分派出去,大多数情况下,NSApp向用户动作发生的NSWindow对象发送sendEvent:消息,
这个NSWindow对象会分派NSEvent消息到与用户动作有关的NSView对象,这些NSEvent消息包含描述消息如mouseDown: or keyDown:等
事件消息的类型不同处理流程也不同。如鼠标事件和触点事件,NSWindow对象直接发送事件到用户按下鼠标或手点按钮的View对象上。如键盘事件,NSApp对象会发送键盘事件到关键窗口(key window)中的first responder 对象。图1-3和图1-4说明这些不同一般的传递路径。
有时候目标视图有可能无法处理该事件,这是就把NSEevet传递给它响应链(见响应链)。
图1-3
[图片上传失败...(image-8aa9b2-1588133958235)]
图1-4
[图片上传失败...(image-c29c1c-1588133958235)]
鼠标事件路径:
如上图1-3 所示 NSWindow 对象最终会把mouse 事件,通过SendEvent方法传递给用户点中的View对象。
如何确定用户点击的是哪一个View对象?
NSWindow对象中有一个保存所有子View的链表,通过对每一个NSView对象发送hitTest消息,判断具体是哪一个子View被用户点中了。
确定了点中的View后,询问View是否愿意成为first responder对象,以便接收后续的key event事件。
如果用户点中的View不在key window中,默认把点中的窗体设置为key window,并且忽略掉这次鼠标点击动作。我们也可以规避这种默认行为,通过重写
NSView的acceptsFirstMouse方法,并直接返回为YES
根据用户点击右键或者左键,以及鼠标的动作,mouse事件分为多种类型,下面是点击左键鼠标类型
|
Action
|
Event type (left mouse button)
|
Mouse-event method invoked (left mouse button)
|
|
Press down the button
|
[NSLeftMouseDown](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/index.html#//apple_ref/c/econst/NSLeftMouseDown)
|
[mouseDown:](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/index.html#//apple_ref/occ/instm/NSResponder/mouseDown:)
|
|
Move the mouse while pressing the button
|
[NSLeftMouseDragged](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/index.html#//apple_ref/c/econst/NSLeftMouseDragged)
|
[mouseDragged:](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/index.html#//apple_ref/occ/instm/NSResponder/mouseDragged:)
|
|
Release the button
|
[NSLeftMouseUp](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/index.html#//apple_ref/c/econst/NSLeftMouseUp)
|
[mouseUp:](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/index.html#//apple_ref/occ/instm/NSResponder/mouseUp:)
|
|
Move the mouse without pressing any button
|
[NSMouseMoved](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/index.html#//apple_ref/c/econst/NSMouseMoved)
|
[mouseMoved:](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/index.html#//apple_ref/occ/instm/NSResponder/mouseMoved:)
|
注意: 因为mouse-moved事件是连续的,他们会很快溢满事件处理机制,所有默认情况下,NSWindow对象不会接受NSApp对象转发的mouse-moved事件。
尽管如此,我们也可以让Window对象接受mouse-moved消息,NSWindow发送 [setAcceptsMouseMovedEvents:](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSWindow_Class/index.html#//apple_ref/occ/instm/NSWindow/setAcceptsMouseMovedEvents:)消息,参数设置为YES。
mouse-moved事件通常会发送给first responder对象,而不是鼠标下的其他View。
相关知识:如果Window中有一个NSTableView,想知道mouse移动到tableView哪一行?
我们知道NSView不会自动响应mouseMove事件,同时NSTableView也不知道mouseMove事件,Cell_base也没有View。
首先解决,要想有mouseMove事件,先得让tableView有焦点。有时候自己手工创建的tableView 由于窗口上有好多View而使得tableView当前不在焦点上,因此可以借住第一响就这个方式来使之成为第一响应。
- (void)focus:(NSWindow *) owner
{
[owner makeFirstResponder:m_tableView];
}
其次还必须把NSTableView 的接受鼠标事件开启:
[m_tableView.window setAcceptsMouseMovedEvents:YES];
好,现在NSTableView有鼠标移动事件了,现在关键是确定鼠标移动点是在哪一行和列上,细看NSTableView的接口你会发现有这样两个方法:
(NSInteger)columnAtPoint:(NSPoint)point;
(NSInteger)rowAtPoint:(NSPoint)point;
键盘事件处理流程:
[图片上传失败...(image-95e12-1588133958234)]
1.NSApp获得key event事件后,首先判断是否是为key equivalents,如果是,把key event事件发送到key window中所有子View,对每个NSView对象发送 [performKeyEquivalent](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/index.html#//apple_ref/occ/instm/NSResponder/performKeyEquivalent:)消息,如果NSWindow对象其中有一个NSView对象返回为YES,表示此NSView对象接收key equivalents事件,反之NSWindow中没有对此key equivalents事件感兴趣,NSApp就把此key equivalents事件发送给菜单栏的菜单项
2.第二步判断是否为key interface control(交互控制键),如tab键,shift+tab键,如果是交互控制键事件,NSApp把消息发送给Key Window的焦点控件。
3.第三步发送key event事件发送给Key Window,Key Window在发送消息给first responder 对象(NSView对象)。
4.第四步判断是否为key Action事件,如向下键,回车键,即key Action事件是物理键值和NSResponder子类对象(View)中的动作绑定而成,如PageDown:等
5.最后作为文本插入。
Responders(响应对象)
1. 只要从NSResponder类继承而来的子类对象就可以直接event事件,或者从响应链中接收event事件。
2. 响应链中有一个first responder对象和一系列next responder对象。
first responder是典型的交互对象,交互对象通过用户点击键盘或者鼠标选择或者激活它,通常first responder 在响应链中第一个接收到event事件或者action消息对象。在interface Builder或者代码中可以设置first responder,NSView 默认不能成为first responder,除非NSView子类实现acceptsFirstResponder,并返回为YES。并且[NSWindow makeFirstResponder:]设置first responder。
注意:NSPanel作为变异的Key window,没有争夺main window的焦点,也就是没有设置自己Window响应链中的first reponder,如果想要设置NSPanel中的NSView对象为的first responder,第一需要NSPanel 中becomesKeyOnlyIfNeeded返回为YES,其次NSView需要 acceptsFirstResponder和NeedsPanelToBecomeKey返回为YES
Responder Chain(响应对象链)
下图为非文档窗体响应链:
[图片上传失败...(image-e61d7d-1588133958234)]
[图片上传失败...(image-34a0b3-1588133958234)]