建材秒知道
登录
建材号 > 链条 > 正文

macOS AppKit 的事件响应简介

壮观的黄豆
留胡子的西装
2023-01-28 17:19:27

macOS AppKit 的事件响应简介

最佳答案
凶狠的裙子
愉快的戒指
2026-04-12 22:19:38

一个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

最新回答
害羞的夏天
炙热的自行车
2026-04-12 22:19:38

Text Kit在继承了Core Text所有强大功能的基础上,将功能封装为面向对象的API,让开发者们都乐开了花。

在这篇教程中,你要去探索Text Kit的各种功能特性,并且你要创建一个简单的但又功能丰富的iPhone笔记应用。

本教程包含一个初始的项目,里面含有事先创建好的UI部分,以便使同学们只关注于Text Kit的部分。在这里可以下载该 项目 。下载完成后解压并在Xcode中打开项目,编译运行后你会看到如下的界面:

该示例应用创建了一个初始的数组用于存放笔记实例,然后在TableViewController中将其渲染出来,当你点击选择某条笔记时,Storyboard和segue会捕获到你的行为,然后处理视图转换的先关工作,使你看到该条笔记的详细信息。

Dynamic Type

Dynamic Type是iOS7中给我们的开发带来变化最多的特性之一,它的作用是让应用中的字体大小遵循你设置的字体大小和粗细。

在iOS7中,打开设置,可以在 通用/辅助功能(General/Accessibility) 和通用/字体大小(General/Text Size) 中查看和设置系统中应用显示字体的属性:

在iOS8中,打开设置,可以在通用/辅助功能/更大字体(General/Accessibility/Larger Text)查看Dynamic Type的文本尺寸。

不管是增加文字粗细还是改变文字大小,在支持Dynamic Type的应用中这都能给用户带来极大的便利,增加了文字的可读性。

为了让应用支持Dynamic Type,你需要设置文本遵循某一 风格 ,而不是明确的指定文本的字体名称和大小。在iOS7中已经为 UIFont 增加了一个新的方法 preferredFontForTextStyle ,它的作用是给创建出一个由用户在设置中根据自身需要设定的风格的字体。

下表中展示了六种不同字体样式的不同大小和粗细程度:

表中最左边的字体是用户可选择的最小的字体,中间是可选择的最大的字体,最右边是选择了辅助功能中给字体加粗后的样式。

最基本的功能支持

实现动态文本的基本功能还是相对较简单的。应用中的字体不再是一个明确的字体,而是需要请求一个特殊的字体样式。在运行时,应用会根据用户在设置应用中对字体的设置以及请求到的字体样式中选择一个合适的字体样式。

到了iOS8,Apple让实现Dynamic Type变得比iOS7更加容易了。尤其是TableView中的默认Label自动支持Dynamic Type,这个很赞!但是如果还想适配iOS7的话那就要在TableView中使用自定义的Label了。所以首先同学们要学会如何在iOS7中处理Dynamic Type,然后你就会发现到了iOS8后,生活是多么美好,晴空万里,没有雾霾!

为什么iOS7是优秀的系统,而iOS8是趋近完美的系统

该教程中初始项目的设置和配置是基于iOS8的。在我们开始学习之前,先编译运行应用,然后尝试改变默认的文本字体大小,多试几次不同的字体大小。你会发现不光是字体大小变了,TableView列表的Cell高度也相应发生了改变。但是你对这个项目还没动过一根手指。同时你也应该发现了点击选择一条笔记后,该笔的详细信息的文本字体却没有发生变化。

但在iOS7中我们的确还要做一些额外的事,没有十全十美的事应该指的就是这个吧。如果你的编译环境是iOS7或iOS8(确保你使用的Xcode版本是6),那么本教程的绝大部分内容都是没问题的。现在我们需要将Xcode的编译环境设置为iOS7已经选择合适的iOS模拟器(iPhone5s)。如果你不打算支持iOS8之前的系统,那么你可以直接使用iOS8的编译环境。

现在咱们在iOS7下编译运行应用,然后重复之前修改字体的操作,你会发现什么?没错,发现了悲剧。应用中的字体会忽略你对字体的设置,根本不起作用。所以,同学们必须要做点什么让Dynamic Type在iOS7跑起来。

打开 NoteEditorViewController.swift ,在 viewDidLoad 方法中添加如下代码:

textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

这里需要注意的是你并没有给 textView.font 设置具体的字体,比如Helvetica Neue之类。相反,你只是请求了一个适合主体文本的字体样式UIFontTextStyleBody 。

然后打开 NotesListViewController.swift ,在tableView(_:cellForRowAtIndexPath:) 方法的 return 语句后添加如下代码:

cell.textLabel?.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)

你依然没有给字体具体的类型,而是请求了一个合适的字体样式。

使用语义接近的字体样式名称,比如 UIFontTextStyleSubHeadline ,可以避免在代码中对字体样式使用硬编码,并确保你的应用中的文本信息能正确的响应用户的设置。

现在再编译和运行应用,你会发现TableView和笔记详细信息页面中的文本字体都发生了变化。下面的截图是设置不同字体大小后的笔记详细信息页面:

目前看起来一起都很完美,但是细心的读者可能会发现这种解决方法只能解决一半的问题。让我们回到设置应用然后再次更改字体大小,然后我们通过后台程序回到我们的笔记应用中,我们发现了什么?没错,文本字体没有响应我们刚才的设置从而发生变化。

我相信我们的用户是不会允许这种事情发生的。又一个挑战出现了,让我们来看看如何解决这个问题。

即时响应字体设置

打开 NoteEditorViewController.swift ,在 viewDidLoad 方法中添加如下代码:

NSNotificationCenter.defaultCenter().addObserver(self,

selector: "preferredContentSizeChanged:",

name: UIContentSizeCategoryDidChangeNotification,

object: nil)

上面这段代码的作用是将我们的 NodeEditorViewController 注册到通知中心里,当字体发生改变时会通知 NodeEditorViewController 中的preferredContentSizeChanged 方法。

我们再来看看 preferredContentSizeChanged 方法:

func preferredContentSizeChanged(notification: NSNotification) {

textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

}

在这个方法中我们就可以改变文本的字体样式了。

注意:这里你可能会有疑惑,已经更改过字体样式了,为什么这里再次请求样式的时候还是 UIFontTextStyleBody 呢?当用户更改完字体样式后,你需要通过 preferredFontForTextStyle 方法重新请求一次字体样式,该方法的参数只代表文本的类型和所在位置,比如是Body里的还是Head中的等,所以 UIFont.preferredFontForTextStyle(UIFontTextStyleBody) 的意思就是请求类型和位置在Body中显示文本的字体样式,每次请求都是获取最新一次设置的字体样式。

打开 NotesListViewController.swift ,重写 viewDidLoad 方法:

override func viewDidLoad() {

super.viewDidLoad()

NSNotificationCenter.defaultCenter().addObserver(self,

selector: "preferredContentSizeChanged:",

name: UIContentSizeCategoryDidChangeNotification,

object: nil)

}

同学们应该注意到了,我们刚才添加的方法和NoteEditorViewController.swift 中添加的方法一样。没错,给NSNotificationCenter 注册的方法是相同的,但是preferredContentSizeChanged 方法会有点区别。

然后在 NotesListViewController.swift 中再添加preferredContentSizeChanged 方法:

func preferredContentSizeChanged(notification: NSNotification) {

tableView.reloadData()

}

上面这段代码的作用是让Tableview重新加载可见的Cell,在更新时就会触发preferredFontForTextStyle() 方法,将新设置的字体样式应用到Tableview的Cell中。

再次编译运行应用,设置字体样式,然后看看我们的应用有没有正确的响应你的设置。