深入探索Cocoa应用程序的事件处理与响应机制
在Cocoa应用程序开发中,了解相关的框架和事件处理机制是至关重要的。下面将详细介绍一些关键的框架以及事件在应用程序中的处理和响应过程。
1. 强大的框架介绍
- PDF Kit :这是一个功能强大的框架,可用于嵌入、显示和创建PDF图像与文档。例如,当你将图标从侧边栏或停靠栏拖出时看到的“噗”动画,实际上就是一个多帧PDF文档。
- Quartz Composer :该框架可执行高级、实时的图像过滤、合成和转换操作。它支持插件架构,能实现无限的效果。
2. 视图对象的作用
视图对象是应用程序与用户之间的中介。它不仅负责显示应用程序的内容,还能将用户的操作转化为可处理的信息。具体来说,视图对象需要感知用户的操作(如按键、移动鼠标),对这些操作进行解释,并将其转化为动作消息发送给控制器对象。
3. 文档模型
在基于文档的应用程序中,对象的组织方式对应用程序如何响应事件以及类的设计有着重要影响。以下是基于文档的应用程序中的关键对象:
-
NSDocumentController
:负责创建新的文档对象(当你从菜单中选择“新建”或“打开”时),将文档类型与实现它们的文档类关联起来,并跟踪当前打开的文档。虽然应用程序通常只定义一种文档类型,但NSDocumentController可以将多种文件类型任意映射到多个文档类。当你选择打开现有文档时,它会根据文件类型确定要实例化的NSDocument子类。
-
建议
:在Xcode中创建基于文档的应用程序项目时,使用Cocoa基于文档的应用程序模板,可节省后续大量工作。
-
NSDocument
:文档的数据模型,负责对文档文件中的数据进行实际的编码和解码。每个文档对象可以在一个或多个窗口中查看,每个窗口由NSWindowController/NSWindow对控制。通常,一个文档只显示在一个窗口中,但也可以允许用户打开同一数据的多个视图。
-
非文档窗口
:与文档对象无关,通常也没有窗口控制器。文档窗口和非文档窗口可以自由混合。
4. 事件和响应者
在Objective - C中,视图和绘图与Java有相似之处,但事件处理却截然不同。用户事件(如鼠标事件、键盘事件、滚动轮事件等)驱动着用户界面。事件处理大致分为两个阶段:事件阶段和响应阶段,每个阶段都由应用程序界面的当前状态动态定义的对象链来完成。
-
事件链
:事件始于操作系统的硬件驱动程序,通过主运行循环进入应用程序,然后传递给应用程序对象,接着沿着文档、窗口、视图和子视图对象的层次结构向下传递,直到找到能够解释该事件的对象。
-
响应者链
:当一个对象解释了事件后,事件就变成了动作。动作会沿着子视图、视图、窗口、文档和应用程序对象的层次结构向上传递,寻找能够响应该动作的对象。
5. 动态应用程序
在Cocoa应用程序中,事件或动作的处理方式由用户界面的当前状态所暗示的对象链决定。下面是一些相关的Cocoa术语:
|术语|描述|
| ---- | ---- |
|活动应用程序|当前活动的、最前端的应用程序。|
|主窗口|单个、最前端的活动应用程序窗口。大多数菜单命令适用于主窗口,主窗口有明显的标题和边框,与后面的非活动窗口区分开来。|
|关键窗口|包含第一响应者的窗口。|
|第一响应者|活动的或具有当前“焦点”的视图对象,通常是响应按键操作的对象,窗口对象也可以是第一响应者。|
|初始第一响应者|非关键窗口中的视图,当该窗口成为关键窗口时,它将默认成为第一响应者,在不是关键窗口的主窗口中有意义。|
例如,用户在主窗口中打开一个电子表格文档,然后打开一个浮动面板并选择一个文本字段。此时,面板中的文本字段成为第一响应者,面板窗口成为关键窗口,但文档窗口仍然是主窗口。而当用户编辑电子表格中的单元格时,该单元格成为第一响应者,文档窗口既是主窗口又是关键窗口。随着用户切换窗口和视图,从第一响应者到顶级应用程序对象的对象链会发生变化,这会隐式地改变应用程序处理事件和动作的方式。
6. 事件处理
6.1 事件对象
事件通过连接到主运行循环的事件端口之一进入应用程序。主运行循环首先将事件封装在一个NSEvent对象中,NSEvent对象具有以下属性:
|属性|描述|
| ---- | ---- |
|type|事件类型,如NSLeftMouseDown、NSLeftMouseDragged等。|
|timestamp|事件发生的时间。|
|window|与事件关联的NSWindow。|
|locationInWindow|事件发生时鼠标在窗口坐标中的位置。|
|clickCount|快速连续发生的鼠标点击次数,用于区分单点击、双击和三击。|
|modifierFlags|事件发生时按下的键盘修饰键(如Shift、Option等)。|
|characters|与按键事件关联的字符串。|
|charactersIgnoringModifiers|与按键事件关联的字符串,不包含任何键修饰符。|
|isARepeat|如果按键事件是由于按住键直到自动重复引起的,则为YES。|
|keyCode|键的虚拟键盘代码。|
并非所有属性都适用于所有事件,还有一些其他属性仅适用于绘图板或力反馈输入设备。虽然你几乎不需要自己创建NSEvent对象,但每个事件处理方法都会将事件作为NSEvent对象接收。
6.2 按键事件
按键事件通常会在对象层次结构中向下分发,以寻找第一响应者。事件类型和多层过滤会严重影响按键事件的分发。按键事件的处理步骤如下:
1. 操作系统将硬件键码转换为用户的当前语言,按键事件通过系统范围的键绑定进行过滤,然后传递给已注册拦截全局按键事件的系统组件,这就是“热键”的处理方式。
2. 按键事件被推送到活动应用程序的事件队列中。
3. 应用程序的主运行循环拉取下一个按键事件,将其包装在NSEvent对象中,并通过 - sendEvent: 消息将其发送到NSApplication对象。
4. 如果按键可能是键等效项(通常是命令 + 键组合),应用程序会向关键窗口发送 - performKeyEquivalent: 消息,关键窗口会对其进行解释或将其传递给第一响应者。如果两者都不响应该事件,则将其发送到应用程序的菜单栏。
5. - sendEvent: 消息被发送到关键窗口。
6. 如果按键是当前活动视图定义的界面控制键(如Tab、右箭头等),窗口和/或控件会将其转换为动作,并向窗口或其第一响应者发送相应的键动作消息。
7. 如果事件不是键等效项或界面控制键,第一响应者将接收 - keyDown:、- keyUp: 或 - flagsChanged: 消息。
8. 如果没有第一响应者且窗口不处理按键事件,则将其传递给菜单栏。
大多数视图只需重写 - keyDown:,可能还需要重写 - keyUp: 或 - flagsChanged: 来自动接收常规按键事件。如果要编写一个编辑文本的视图,视图对象必须符合NSTextInput协议。
6.3 鼠标事件
鼠标和触摸事件根据用户界面的几何形状进行分发,鼠标事件总是与一个位置和一个窗口相关联,用于确定事件的接收者。
-
鼠标按下事件
:
1. 当鼠标被点击时,系统的WindowServer确定哪个应用程序应接收该事件。
2. 应用程序的主运行循环通过 - sendEvent: 消息将鼠标事件发送到NSApplication对象。
3. NSApplication根据事件的位置确定事件发生的窗口,并通过另一个 - sendEvent: 消息将事件转发到该窗口。
4. 窗口的 - sendEvent: 方法使用位置找到与点击对应的子视图,并向该视图发送 - mouseDown: 消息。
第一个鼠标按下事件会尝试使视图成为第一响应者,使其成为按键事件的接收者,其窗口成为关键窗口,并重新定义响应者链。要符合条件,视图在接收到 - acceptsFirstResponder 消息时必须返回YES。默认情况下,NSView返回NO。如果视图同意,它将被指定为第一响应者并接收 - becomeFirstResponder 消息。当窗口停用或另一个视图成为第一响应者时,该视图将接收匹配的 - resignFirstResponder 消息。此外,视图还可以控制“点击穿透”行为,通过重写 - acceptsFirstMouse: 方法来改变默认的点击处理方式。
-
鼠标拖动和鼠标释放事件
:视图接收到 - mouseDown: 消息后,可能会收到 - mouseDragged: 和 - mouseUp: 消息。这些消息统称为鼠标跟踪消息。即使鼠标被拖出视图的框架,同一视图对象仍会接收后续的 - mouseDragged: 和 - mouseUp: 消息。- mouseDragged: 仅在鼠标按钮按下并移动时发送。如果要让视图在鼠标拖动手势期间执行特定操作,可能需要重写这三个方法。如果只关注点击行为,重写 - mouseUp: 并测试事件的位置即可。
下面是TicTacToe项目中的 - mouseUp: 事件处理方法:
- (void)mouseUp:(NSEvent*)theEvent
{
NSPoint loc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
SquareIndex index;
for (index=0; index<9; index++) {
if (NSPointInRect(loc,[ChalkboardView rectOfSquare:index])) {
[[self document] playerClickedSquare:index];
break;
}
}
}
该方法通过NSEvent对象接收鼠标事件信息,首先将事件位置转换为视图的本地坐标系,然后检查用户是否在游戏板的九个方块内释放了鼠标。如果是,则视图向其控制器对象发送 - playerClickedSquare: 消息,以进行移动并更新游戏板。
另一种处理鼠标拖动事件的方法是停留在 - mouseDown: 方法中并启动一个嵌套运行循环,直到拖动完成。这本质上是一个“模态”鼠标拖动循环,会阻止应用程序的其余部分执行,直到拖动结束。
6.4 鼠标跟踪
鼠标拖动时的移动会自动提供,但鼠标按钮抬起时的移动通常会被忽略。不过,在某些情况下,你可能希望在界面中接收正常鼠标移动的通知,有两种技术可以实现:
-
请求窗口的鼠标移动事件
:
1. 将包含视图的窗口的acceptsMouseMovedEvents属性设置为YES。
2. 在子视图中实现 - mouseMoved: 方法。
3. 设置acceptsMouseMovedEvents后,窗口将开始向其视图发送 - mouseMoved: 消息,就像分发其他鼠标事件一样。
-
定义鼠标跟踪矩形
:
1. 在一个对象中实现 - mouseEntered:、- mouseExited:、- mouseMoved: 和 - cursorUpdate: 方法,这个对象可以是视图对象或其他对象。
2. 使用要跟踪的区域的矩形(在视图坐标中)和步骤1中创建的所有者对象创建一个NSTrackingArea对象。你还可以指定其他跟踪选项或提供上下文信息字典。
3. 通过向NSView对象发送 - addTrackingArea: 消息将NSTrackingArea对象附加到该视图。
4. 附加后,所有者对象将接收鼠标进入、移动、退出和光标更新事件。
7. 响应者链
响应者链是从第一响应者开始,向上贯穿对象层次结构的对象序列。它是确定应用程序如何响应命令和其他动作的主要机制。响应者链处理动作消息(形式为 - (IBAction)action:(id)sender 的Objective - C消息)的过程如下:
1. 检查链中的每个对象是否响应(实现)给定的消息。
2. 如果对象响应,则接收该消息;如果不响应,则检查链中的下一个对象,直到找到响应消息的对象或没有更多对象为止。
响应者链中的一些特殊情况:
- 当窗口失去关键窗口状态时,它会记住之前的第一响应者,该对象成为窗口的初始第一响应者。当窗口再次成为关键窗口时,初始第一响应者会自动成为第一响应者(假设在此过程中没有其他视图成为第一响应者)。
- 当关键窗口和主窗口不同时,响应者链从第一响应者开始,经过关键窗口,然后到主窗口的初始第一响应者。
响应者链的大部分部分是可选的,很少会遍历整个链。例如,如果关键窗口没有第一响应者对象,响应者链将从关键窗口本身开始;如果关键窗口和主窗口相同,响应者的父视图层次结构只会遍历一次;没有控制器的窗口不会在链中包含窗口控制器;非文档窗口没有控制器或文档对象,会直接跳到应用程序;窗口和应用程序对象可能没有委托;非基于文档的应用程序没有文档控制器对象,其响应者链以应用程序或其委托结束。
在开发iPhone应用程序时,响应者链的原理相同,但响应者链往往比较简单。因为iPhone应用程序一次只显示一个UIWindow,所以关键窗口和主窗口始终是同一个。传递给UIView的动作首先会传递给其UIViewController(如果有的话),然后再传递给其包含的UIView。
8. 动作消息
动作、菜单状态查询、服务和错误解释都通过响应者链进行过滤。下面通过一个示例来说明响应者链如何与应用程序集成。假设有一个应用程序,有四个菜单命令:复制、关闭、保存和新建,每个命令分别发送 - copy:、- performClose:、- saveDocument: 和 - newDocument: 动作消息到第一响应者:
-
复制命令
:- copy: 动作消息发送到第一响应者。如果第一响应者是一个实现了剪贴板功能的可编辑文本字段,它会将当前选择的内容传输到应用程序的粘贴板(剪贴板)。
-
关闭命令
:如果第一响应者和其所有父视图都没有实现 - performClose: 方法,关键窗口会响应该命令并关闭自身。
-
保存命令
:如果第一响应者、其所有父视图、关键窗口、其委托、主窗口中的视图、主窗口、其委托或窗口控制器都没有实现 - saveDocument: 消息,NSDocument对象会负责对活动文档的内容进行编码并将其写入文件。
-
新建命令
:- newDocument: 消息会遍历响应者链,直到到达为基于文档的应用程序创建的单个文档控制器对象,文档控制器会创建一个新文档并打开一个无标题窗口。
应用程序对命令的响应会根据响应者链的状态而变化:
- 如果另一个视图对象是第一响应者,应用程序可能不会响应复制命令。
- 应用程序中的其他窗口会忽略关闭动作,因为它们不在响应者链中。
- 如果主窗口不是文档窗口,其响应者链中没有NSDocument对象,该窗口将不会响应保存动作。
- 应用程序总是会响应新建命令,因为链中的顶级对象始终会响应该命令。
9. 发送动作消息
- 目标动作 :发送到特定对象的动作消息称为目标动作。
- 非目标动作 :发送到响应者链的动作称为非目标动作。动作由Objective - C消息标识符(SEL)和对象标识符(id)组成。如果目标标识符指向一个对象,则为目标动作;如果目标标识符为nil,则为非目标动作。
动作通过向NSApplication对象发送 - sendAction:to:from: 消息来发送。目标(to:)参数可以是一个对象(目标动作)或nil(非目标动作)。应用程序会确定目标是否响应动作,或者尝试在响应者链中找到响应动作的目标。如果成功,目标对象将接收动作消息,并且 - sendAction:to:from: 返回YES。你可以通过编程方式发送动作,但NSControl视图、菜单项和键盘等效项通常会为你发送动作。
在Interface Builder中,动作被定义为消息/对象对。如果动作直接连接到NIB文档中的另一个对象,则定义为目标动作;将动作连接到第一响应者占位符NIB对象会创建一个非目标动作,该动作将在运行时传递给当前的第一响应者。
10. 菜单动作
Cocoa应用程序中的菜单项是响应者链的另一个重要应用。当你下拉应用程序菜单时,只有适应当前界面状态的菜单项才会启用,不适当的菜单项会被禁用。Cocoa框架通过检查响应者链中的对象来动态确定菜单项的启用状态。每个菜单项都与一个动作相关联,当用户下拉菜单时,会搜索响应者链以找到能够响应每个菜单项的对象。如果链中有对象响应某个动作,则该菜单项启用;否则,禁用。
要在应用程序中实现一个自动启用和禁用的菜单命令,可按以下步骤操作:
1. 在负责实现命令的对象中创建一个动作方法:- (IBAction)action:(id)sender。
2. 在Interface Builder中创建一个菜单项,并将其动作设置为步骤1中的消息标识符,连接到第一响应者。
例如,在TicTacToe项目中,TTTDocument对象实现了 - reset: 和 - playForPlayer: 两个动作,“重置”和“走一步”菜单项会发送这些动作,还有一个“重置”按钮也会发送相同的动作。你可以打开和关闭TicTacToe文档窗口,观察这些操作如何影响菜单项的状态。
11. 禁用动作菜单项
虽然基于响应者链中的对象自动显示菜单项很方便,但有时响应者链中的对象实现了某个动作,但该动作并非总是合适的。Cocoa框架通过响应者链可以轻松实现禁用菜单项的功能。具体做法是,当找到菜单项的响应者后,菜单框架会检查该对象是否实现了 - validateMenuItem: 方法。如果实现了,会向该对象发送该消息,传递要考虑的菜单项。如果对象返回YES,则菜单项启用;否则,菜单项将被禁用,就像对象不响应该动作一样。
在TicTacToe项目中,“重置”和“走一步”菜单项都会向响应者链发送动作。如果不做其他处理,只要文档对象在响应者链中,这两个菜单项都会启用。但游戏的重置命令只有在游戏开始后才有意义,而走一步命令只有在轮到用户移动时才适用。以下是实现 - validateMenuItem: 方法的代码:
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
{
if ([menuItem action]==@selector(reset:)) {
return game.isStarted;
} else if ([menuItem action]==@selector(playForPlayer:)) {
return (game.nextPlayer==USER_PLAYER);
}
return [super validateMenuItem:menuItem];
}
该代码通过检查菜单项的动作来确定要验证的命令,然后根据游戏的状态确定菜单项是否应该启用。你可以使用任何识别方法来识别菜单项,对象只会接收它响应的动作的 - validateMenuItem: 消息。
通过以上对Cocoa应用程序中各种框架、事件处理和响应者链的介绍,我们可以更深入地理解如何开发出高效、灵活且用户体验良好的应用程序。在实际开发中,合理运用这些机制能够让应用程序更好地响应用户的操作,提升应用的性能和可用性。
深入探索Cocoa应用程序的事件处理与响应机制
12. 响应者链的灵活性与应用场景
响应者链的灵活性使得它在不同的应用场景中都能发挥重要作用。以下是一些常见的应用场景及响应者链如何适应这些场景:
-
多窗口应用程序 :在多窗口应用程序中,不同的窗口可能有不同的第一响应者。响应者链可以确保每个窗口内的事件和动作都能正确处理。例如,当用户在一个窗口中进行操作时,该窗口的第一响应者及其响应者链会处理相关事件;而其他窗口不会干扰这些操作。
-
复杂界面布局 :对于具有复杂界面布局的应用程序,可能存在多个嵌套的视图和子视图。响应者链可以有效地处理这些视图之间的事件传递。例如,当用户在一个嵌套较深的子视图中进行操作时,事件会通过响应者链向上传递,直到找到合适的处理对象。
-
动态界面更新 :在应用程序运行过程中,界面可能会动态更新,例如添加或移除视图。响应者链能够自动适应这些变化,确保事件和动作的处理不受影响。例如,当一个新的视图被添加到界面中时,它会自动融入响应者链,接收和处理相关事件。
13. 优化事件处理和响应者链的性能
为了提高应用程序的性能,我们可以对事件处理和响应者链进行优化。以下是一些优化建议:
-
减少不必要的事件处理 :避免在视图中处理不必要的事件。例如,如果一个视图不需要处理鼠标移动事件,就不要实现 - mouseMoved: 方法。这样可以减少系统资源的消耗。
-
合理设置第一响应者 :确保第一响应者的设置合理。如果一个视图不需要成为第一响应者,就不要让它成为第一响应者。这样可以避免不必要的事件传递和处理。
-
缓存响应者链信息 :在某些情况下,可以缓存响应者链的信息,避免每次都重新遍历响应者链。例如,在一个频繁处理相同动作的应用程序中,可以缓存响应某个动作的对象,下次需要处理该动作时直接调用缓存的对象。
14. 事件处理和响应者链的流程图
下面是一个简单的mermaid流程图,展示了事件处理和响应者链的基本流程:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([事件发生]):::startend --> B(操作系统硬件驱动):::process
B --> C(主运行循环):::process
C --> D(NSApplication对象):::process
D --> E{事件类型}:::decision
E -->|按键事件| F(关键窗口):::process
E -->|鼠标事件| G(根据位置确定窗口):::process
F --> H(第一响应者):::process
G --> I(子视图):::process
H --> J{是否处理事件}:::decision
I --> J
J -->|是| K(事件转换为动作):::process
J -->|否| L(继续查找处理对象):::process
L --> M(响应者链):::process
K --> M
M --> N{是否找到响应对象}:::decision
N -->|是| O(执行动作):::process
N -->|否| P(结束处理):::process
O --> P
15. 总结与展望
通过对Cocoa应用程序中事件处理和响应者链的深入探讨,我们了解到它们在应用程序开发中的重要性。事件处理机制确保了用户与应用程序之间的交互能够正确响应,而响应者链则提供了一种灵活且高效的方式来处理这些事件和动作。
在未来的应用程序开发中,随着用户界面的不断发展和复杂化,事件处理和响应者链的机制也将面临新的挑战和机遇。例如,随着虚拟现实和增强现实技术的普及,用户与应用程序的交互方式将更加多样化,这就需要事件处理机制能够适应这些新的交互方式。同时,为了提高应用程序的性能和响应速度,我们还需要不断优化事件处理和响应者链的算法和实现。
总之,掌握Cocoa应用程序中的事件处理和响应者链机制是开发高质量应用程序的关键。希望本文能够为开发者提供一些有用的参考和指导,帮助他们更好地开发出满足用户需求的应用程序。
16. 常见问题解答
以下是一些关于Cocoa应用程序事件处理和响应者链的常见问题解答:
-
问题1 :为什么我的视图无法成为第一响应者?
答:视图要成为第一响应者,需要满足一些条件。首先,视图在接收到 - acceptsFirstResponder 消息时必须返回YES。默认情况下,NSView返回NO,如果你希望视图成为第一响应者,需要重写该方法并返回YES。其次,视图所在的窗口必须是关键窗口。 -
问题2 :如何手动触发一个动作消息?
答:你可以通过向NSApplication对象发送 - sendAction:to:from: 消息来手动触发一个动作消息。例如:
SEL actionSelector = @selector(someAction:);
id target = someObject;
id sender = self;
[[NSApplication sharedApplication] sendAction:actionSelector to:target from:sender];
-
问题3
:响应者链的遍历顺序是固定的吗?
答:响应者链的遍历顺序不是完全固定的,它会根据应用程序的当前状态动态变化。例如,当第一响应者发生变化时,响应者链的起始点也会改变;当窗口的状态发生变化时,响应者链的路径也可能会改变。
通过以上的介绍,我们对Cocoa应用程序的事件处理和响应者链有了更全面的了解。在实际开发中,我们可以根据具体的需求和场景,灵活运用这些机制,开发出更加优秀的应用程序。
超级会员免费看
12

被折叠的 条评论
为什么被折叠?



