Cocoa:应用内键盘事件处理

本文将介绍 Cocoa 中应用内(不包含全局快捷键)键盘事件处理路径,如何在路径的每个阶段重载相应的方法来处理事件。

前言

本文详细介绍了按键事件路径及处理要点概念,主要翻译了官方文档 Handling Key Events。具体实现可以参看另外两篇文章:键盘事件处理:Key Equivalents键盘事件处理:Keyboard Actions

了解 Cocoa Key Events

macOS 系统在用户按下键盘按键(一个或者好几个)时会产生键盘事件。当键盘事件由不止一个按键触发时,可视为某一主要按键被其他按键修饰。常用的修饰键有 Command, Control, Option (Alt), Shift。

键盘事件有如下基本事实:

  • 键盘事件有三种类型(NSEventType),对应 NSResponder 的方法:
    • NSKeyDown keyDown:
    • NSKeyUp keyUp:
    • NSFlagsChanged flagsChanged: 当 Modifier Keys 单独被按下时,将触发 flagsChanged: 方法
  • 对于应用而言,键盘事件有四种含义:
    • Key equivalent
    • Keyboard interface control command
    • Keyboard action
    • Character
  • 键盘事件的处理路径为:全局应用对象 NSApp 首先判断其是否为 Key equivalent,其次判断其是否为 Keyboard interface control command,并分别处理。
  • 如果两者皆非,NSApp 将 dispatch event 至活跃窗口对应的 NSWindow 对象,NSWindow 向 First responder 发送 keyDown: 消息。
  • Responder 将 Key event 分为三类并分别处理:
  • First responder 不处理时,将沿 Responder chain 向上传递。

按键事件对象即 NSEvent,与「按键」行为相关的属性有:

  • characterscharactersIgnoringModifiers :按键事件的文本信息。charactersIgnoringModifiers 将忽略除 Shift 之外的修饰键。这两个属性都是复数形式,因为一次按键(此处为动词,key stroke)也会产生多个字符(例如 à 对应 a`)
  • modifierFlags:判断 Key event 是否包含 Modifier key
  • isARepeat:同一按键是否被多次连续的按下

下文将具体讨论 NSApp 在键盘事件处理路径上的行为。

按键事件的路径

按键事件产生后,NSApp 接收到一个按键事件。NSApp 根据按键事件的含义做出不同的处理,下图是一个按键事件在应用中真正被处理之前可能经过的路径:

按照 NSApp 对按键事件的处理顺序,有三个关键步骤,具体说明如下:

Key Equivalents

Key Equivalents 即按键或者按键组合对应至某些菜单操作或者应用内置快捷键操作,按键即触发操作。

具体过程是:

  • 首先,NSApp 向活跃窗口发送 performKeyEquivalent:  消息,并沿着  View Hierarchy  向下传递 (NSView 中该方法默认实现即向 subViews 依次发送该消息),直到某个对象返回 true
  • 如果视图层次中没有对象处理事件, NSApp 便将 performKeyEquivalent: 消息发送给 Menu 的控件,直到某个对象返回 true
  • 如果 Menu 中也没有对象处理事件,进行下一步

Apple 的文档中不建议继承自 NSWindow 的类重载 performKeyEquivalent: 方法。
某些 Cocoa Classes,建议利用如 NSButton , NSMenu , NSMatrix , NSSavePanel 等默认实现了 performKeyEquivalent: 方法的控件,可以通过设置其 keyEquivalent 属性为对应的键值,来实现键值匹配时控件自动触发点击事件。 在重载 performKeyEquivalent: 方法时,应该利用 NSEvent 对象的 charactersIgnoringModifiers 属性来判断触发事件的按键(具体判断方法参考下文重载 keyDown:)并返回 true;如果未对事件进行处理,方法应该调用父类的实现或者(当你清楚地知道父类也不会处理事件时)返回 false,即阻止事件继续沿 View Hierarchy 向下传递。

重载 performKeyEquivalent: 伪代码

override func performKeyEquivalent(with event: NSEvent) -> Bool {
    // Pseudo-code 
    let key = event.charactersIgnoringModifiers
    if key == key that we wanted {
        // handle things as you will do in keyDown:
        return true // tell Application that we handled it
    } else {
        if superView will return no anyway {
            return false
        } else {
            super.performKeyEquivalent(with: event)
        }
    }

更详细的内容可以参考 Handling Key Equivalents

Keyboard Interface Control

Keyboard Interface Control 指的是特定的按键事件视为界面操作命令(例如 Tab 和 Shift + Tab 将转移当前焦点至另一窗口,Space 模拟鼠标点击操作,选中某些可选项等)。大多数参与这一过程的界面元素属于 NSControl,但并不全是。当 Object 为当前操作焦点时,Application Kit 默认会将其 border 绘制为浅蓝色的 key-focus ring。Key Interface Control 的具体按键与命令对应列表可参看 Keys used in keyboard interface control

具体过程是:

  • NSWindow 默认以 First Responder 为起点,通过 NSView 的 nextKeyViewpreviousKeyView 属性组成首尾相连的 Key-view loop
  • NSWindow 会将特定按键或按键组合事件与控制当前 Key-view 的 Commands 绑定(如 Tab 键切换到 nextKeyView 等)
  • 如果按键不属于特定按键,或者特定按键的对应 Command 没有实现,进行下一步

让 NSView 参与 Key-view loop 需要:

  • 继承 NSView 并 acceptsFirstResponder 返回 true
    • 这样做是为了让 canBecomeKeyView 返回 true,canBecomeKeyView 不但需要 acceptsFirstResponder 为 true,还受到视图是否可见、窗口是否支持键盘操作的影响
  • 通过设置 nextKeyView 属性影响 Key-view loop

更详细的内容可以参考 Keyboard Interface Control

Keyboard Actions

不像 Action messages,Keyboard actions 是 Command(代表 NSResponder 的特定方法)。Keyboard actions 根据键值绑定规则(Text System Defaults and Key Bindings)与物理按键行为绑定(参考 Default Mac OS X System Key Bindings)。

例如:

/* ~/Library/KeyBindings/DefaultKeyBinding.dict */
{
    /* Additional Emacs bindings */
    "~v" = "pageUp:";
}

具体过程是:

  • NSApp 通过 sendEvent: 将按键事件传给活跃窗口,NSWindow 调用 First Responder 的 keyDown: 方法(当只有修饰键时,调用 flagsChanged: ),并沿着 Responder Chain 向上传递 (NSResponder 中该方法默认实现即将消息传递给 Next Responder),直到某个 Responder 响应并处理事件。
  • 如果没有 Responder 响应,进行下一步。

大多数 responder 对象用重载 keyDown: 来处理键盘事件。文本对象将输入文字,其他对象根据需要处理某些 Key event,例如 Delete 删除当前选中对象。

NSResponder 中 keyDown: 的默认实现是将消息发送给 Next responder,当 Next responder 为空时将 Beep。
- 如果要处理的 Key event 与 Command 已根据规则绑定:Responder 可以通过实现 Command 并在 keyDown: 中调用 interpretKeyEvents: 将事件传递给系统 Input Manager。interpretKeyEvents 将向调用者发送 doCommandBySelector: ,执行对应的 Command。 - 如果要处理特殊键盘事件,可以通过 event.charactersIgnoringModifiers, event.modifierFlags 与 Commonly-used Unicode characters, Function-Key Unicodes, NSEventModifierFlags 中的常量来判断特殊按键。

注意:NSResponder 的某些 Command 仅声明而没有实现,因此调用 super 有可能抛出异常。

重载 keyDown: 伪代码实现: 对于绑定了 Command 的按键组合:

override func keyDown(with event: NSEvent) -> Bool {
        self.interpretKeyEvents([event])
}

// assume we want to handle 'esc', which maps to cancelOperation
override func cancelOperation(_ sender: Any?) {
    // if our super class is NSResponder, we can't call super
    // it is ought to implemented by subclasses

    // do things..
}

对于特殊的按键组合:

override func keyDown(with event: NSEvent) -> Bool {
''      // Pseudo-code 
    let chars = event.charactersIgnoringModifiers
    let modifiers = event.modifierFlags

    if chars and modifiers we want to handle {
        // do our things.
    } else {
        super.keyDown(with: event)
    }
}

讨论

1. NSEvent 的 keyCode 是什么?

在讨论如何处理键盘事件的问题中,一般的回答是在 keyDown: 方法中通过 theEvent.keyCode 与固定的某些值进行判断。Apple 的 Guide 中却完全没有提到 keyCode ,在文档中对这一属性的说明:

The property’s value is hardware-independent. The value returned is the same as the value returned in the kEventParamKeyCode when using Carbon Events.

可能在 Cocoa 调用 Carbon 键盘处理相关函数时需要用到 keyCode 属性进行转换。

2. NSEvent 的 Characters 是什么?

在判断是否按下 'A' 键时,常用的方法也有判断 theEvent.characters 或者 theEvent.charactersIgnoringModifiers 是不是 'A' 来实现。

这样的问题在于,如果切换了输入法之后,同一按键的值是有可能会变的。实际上由于 characters 发生变化,在代码中似乎并没有除了 keyCode 之外合适的途径来判断按下的究竟是哪个键了。

在 Menu 中设置的 Key Equivalent 则不受输入法的影响,可以正常工作,猜测这一方法最终实现依赖 keyCode。

另外在常用软件 Sketch 中,切换了奇怪的输入法 Menu 中的快捷键也无法响应,猜测 Sketch 的 Menu 中的 Button 是自定义实现的,并且很有可能是用 characters 来判断按键事件。

3. Key Equivalents 与 Modifier Key

Handling Key Equivalents 中提到

This binding causes that view to perform a specified action when the user types that character, typically while pressing a modifier key (in most cases the Command key). A key equivalent must be a character that can be typed with no modifier keys, or with Shift only.

一般 Key Equivalents 处理的按键事件是同时按下 Modifier Key 的,但 Key Equivalent 必须是不需要 Modifier Key 或者仅需要 Shift 的按键。并不理解这里的意思。


That's All, Happy Coding 😄

参考

广告

CurrencyX 是我们开发的 Mac 上小而美的汇率 app,售价¥18,如果你觉得文章有用,可以前往 App Store 买一个支持我们 点击下载 CurrencyX