在本节的例子中,会自定义很多UI控件实现不同的事件响应,如下图所示:
IOKit 事件框架
事件流程
OS X的事件依赖 IOKit 框架,事件发生后首先会传递到IOKit框架中处理,然后通知Window Server服务层处理,由Window Server存储到到一个FIFO队列中,再逐一转发到当前的Active Window或Active App中进行处理;
- 每个应用都有一个 Main Run Loop,它会一直遍历系统的 FIFO Event Queue队列,如果有属于自己的事件,则会通过NSApp的senEvent()方法转发给NSWindow,再经NSWindow转发给NSView的合适对象去处理;
- 所有继承了NSView的控件都可以响应事件,其它的还有NSApplication、NSWindow、NSDrawer、NSWindowController也可以响应事件;
First Responsers
First Responders称为事件的第一响应者;AppKit中的事件都处于一个响应的链条中,这个链条是由一个叫做NSResponder
的类定义。这个响应链条其实是一个列表,它里面装满了能够响应用户事件的对象,当用户点击鼠标,或者按下键盘的某个键,或者触摸触控板都会生成Event
事件,然后在响应链条中寻找可以处理这个事件的对象对事件进行处理。一个对象如果可以处理事件,那么这个对象必须继承自NSResponder
这个类。
一个NSResponder实例对象有三个组件:事件消息(鼠标,键盘,触控板等产生的)、动作消息(比如NSButton 执行target 的action 方法,就属于一种action消息)、响应链条。
事件拦截
此功能相当于web开发中的拦截器一样,真实开发中不太常用,多数用于调试使用。
import Cocoa
class Window: NSWindow {
override func sendEvent(_ event: NSEvent) {
NSLog("sendEvent \(event)")
//对event做统计处理,内部转发处理
super.sendEvent(event)
}
override func postEvent(_ event: NSEvent, atStart flag: Bool) {
NSLog("sendEvent \(event)")
super.postEvent(event,atStart:flag)
}
}
- sendEvent():可以拦截到所有的事件消息,在这里可以统一转发或特殊处理;
- postEvent():分发事件;
事件监控
先在ViewController中添加事件监听器,以便后面观察,,真实开发中不太常用,多数用于调试使用,可定义两类事件:
- startGlobalEventMoniter():全局事件,即监听鼠标和键盘的所有事件;
- startLocalEventMoniter():局部事件,只监听当前应用上发生的事件;
监控器添加
import Cocoa
class ViewController: NSViewController {
//定义事件类型:全局和局部,局部是指在控件上操作
var gEventHandler: Any?
var lEventHandler: Any?
override func viewDidLoad() {
super.viewDidLoad()
//全局事件监听
self.startGlobalEventMoniter()
//局部事件监听
self.startLocalEventMoniter()
self.view.window?.makeFirstResponder(self)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func startGlobalEventMoniter() {
self.gEventHandler = NSEvent.addGlobalMonitorForEvents(
matching: [NSEvent.EventTypeMask.keyDown,NSEvent.EventTypeMask.leftMouseDown],
handler: {
event in print("全局事件: \(event)")
}
)
}
func startLocalEventMoniter() {
self.lEventHandler = NSEvent.addLocalMonitorForEvents(
matching: [NSEvent.EventTypeMask.keyDown,NSEvent.EventTypeMask.leftMouseDown],
handler: {
event in print("控件事件 \(event)")
return event
}
)
}
// func stopEventMoniter() {
// NSEvent.removeMonitor(self.gEventHandler!)
// NSEvent.removeMonitor(self.lEventHandler!)
//}
}
监控器删除
在窗口或页面关闭时删除监控,需要手工调用一下,这个方法可在NSWindow中调用。
func stopEventMoniter() {
NSEvent.removeMonitor(self.gEventHandler!)
NSEvent.removeMonitor(self.lEventHandler!)
}
事件分发(Event Dispatch)
在主事件循环中(main event runloop
),应用程序对象(NSApp)会不断的从事件队列中(event queue)获取最前面的事件,然后将它转换为NSEvent 对象后,派发到最终目标.
NSApp
是通过nextEventMatchingMask:untilDate:inMode:dequeue:
这个方法从事件队列中获取到事件,当事件队列为空的时候(也就是队列中无事件),这个方法会阻塞,直到有新的事件到来才会继续.NSApp
将事件转换为NSEvent后,第一件事就是调用sendEvent:方法进行派发.- 大部分的情况下,
NSApp
都会将事件派发给用户操作的那个窗口(NSWindow),这是通过调用窗口(NSWindow)的sendEvent:方法完成的. NSWindow
窗口对象将事件以NSResponder Message
消息的形式(比如mouseDown:
或者keyDown:
)派发到与用户操作关联的NSView
对象.NSWindow
派发事件时会根据事件类型略有不同:对于鼠标和触控板事件,NSWindow
对象会将事件派发到用户鼠标点击的NSView.对于键盘(keyboard
)事件,NSWindow
通常会将事件派发给key Window
的第一响应者
由此可见,在事件派发的过程中,会根据事件种类(AppKit中定义的NSAppKitDefined
类型)的不同而进行不同的派发选择.有些事件只能由NSWindow或者NSApplication自身来处理,比如应用的隐藏/显示/激活状态/失去激活状态等。
其他事件派发
在应用程序中,我们可以使用NSTrackingArea
类添加一个监控区域,这些事件NSWindow
对象会直接派发到拥有这个区域的指定对象(通常发送 mouseEntered:和 mouseExited:消息).
应用程序(NSApplication)生成的周期性事件(NSPeriodic)通常不会使用sendEvent:
派发,它们是通过某个NSObject对象注册后(通过调用nextEventMatchingMask:untilDate:inMode:dequeue: 方法)才会得到处理.具体的详细内容,可以参考Other Types of Events
NSEvent对象
下列数据为Event的输出:
NSEvent:
type=LMouseDown # 事件类型,此为鼠标左键点击
loc=(1538.98,531.011) # 鼠标当前点击的屏幕位置,屏幕原点为左上角
time=73377.2
flags=0
win=0x0
winNum=79
ctxt=0x0
evNum=15613
click=1 # 鼠标点击次数,如果是双击则此值为2
buttonNumber=0
pressure=1
deviceID:0x40000004be0f1e7
subtype=NSEventSubtypeTouch
Mouse 鼠标事件
鼠标事件主要分为点击、 拖放两类,同时需要区域跟踪以及鼠标位置这两方面数据的支撑,如果需要处理鼠标坐标,一般还要转换成视图坐标再处理。
鼠标事件绑定
鼠标事件属于控件代理中的抽象方法,只需要覆盖实现即可。
//定义在相应的ViewController中
override func mouseDown(with event: NSEvent) {
print("mouseDown left \(event)")
}
- mouseDown:左键按下
- rightMouseDown:右键按下
- mouseUp:左键松开
- rightMouseUp:右键松开
判断特殊键
判断点击鼠标时,是否同是按下了特殊键:
//如果同时按下了command键
if event.modifierFlags.contains(NSEvent.ModifierFlags.command) {
self.wantsLayer = true
self.layer?.borderColor = NSColor.gray.cgColor
self.layer?.borderWidth = 10
}
判断鼠标双击
判断Event事件参数
//判定是否鼠标双击事件
if event.clickCount>=2 {
NSLog("mouse double click event ")
}
else {
super.mouseDown(with: event)
}
实现鼠标拖放事件
鼠标拖放是一个连续的事件,按mouseDown -> mouseDragged -> mouseUp这样顺序执行,这也是拖放功能的核心实现。
class DragView: NSView {
var dragged: Bool = false
var centerBox: NSRect?
override func awakeFromNib() {
super.awakeFromNib()
centerBox = self.bounds.insetBy(dx: 10,dy: 10);
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
if(dragged){
NSColor.blue.setFill()
}
else {
NSColor.green.setFill()
}
dirtyRect.fill()
}
override func mouseDown(with event: NSEvent) {
//获取鼠标点击位置坐标
let eventLocation = event.locationInWindow
//转化成视图的本地坐标
let pointInView = self.convert(eventLocation, from: nil)
//判断当前鼠标位置是否在中心点附近范围内
if NSPointInRect(pointInView, centerBox!) {
dragged = true
}
}
override func mouseDragged(with event: NSEvent) {
if dragged {
let eventLocation = event.locationInWindow
let positionBox = NSRect(x: eventLocation.x, y: eventLocation.y, width: self.frame.size.width, height: self.frame.size.height)
//更新视图位置
self.frame = positionBox
//重绘界面
self.needsDisplay = true
}
}
override func mouseUp(with event: NSEvent) {
dragged = false
self.needsDisplay = true
}
//鼠标指针形状
override func cursorUpdate(with event: NSEvent) {
NSCursor.crosshair.set()
}
}
实现鼠标事件跟踪
出于对性能的考虑,一般拖放时需要设置一个可拖放的监控区域,只有鼠标进入到此区域时才可以触发拖动,所以设计一个鼠标的监控区域的示例代码如下,完整上述不完整的拖放代码。
class TrackView: NSView {
var tracking: Bool = false
override func awakeFromNib() {
super.awakeFromNib()
//监控区域
let eyeBox = CGRect(x: 0, y: 0, width: 80, height: 80)
let trackingArea = NSTrackingArea(rect: eyeBox, options: [NSTrackingArea.Options.mouseMoved,NSTrackingArea.Options.mouseEnteredAndExited,NSTrackingArea.Options.activeInKeyWindow,NSTrackingArea.Options.cursorUpdate ], owner: self, userInfo: nil)
self.addTrackingArea(trackingArea)
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
if(tracking){
self.wantsLayer = true
self.layer?.borderWidth = 10
self.layer?.borderColor = NSColor.red.cgColor
}
else {
self.wantsLayer = true
self.layer?.borderWidth = 1
self.layer?.borderColor = NSColor.gray.cgColor
}
}
//进入区域
override func mouseEntered(with event: NSEvent) {
tracking = true
self.needsDisplay = true
}
override func mouseMoved(with event: NSEvent) {
}
//离开区域
override func mouseExited(with event: NSEvent) {
tracking = false
self.needsDisplay = true
}
//鼠标光标更新为十字架形状
override func cursorUpdate(with event: NSEvent) {
NSCursor.crosshair.set()
}
//快捷键,
override func performKeyEquivalent(with event: NSEvent) -> Bool {
let characters = event.characters
if characters == "i" {
NSApp.terminate(self)
return true
}
return false
}
}
以上差不多就是鼠标事件的基本内容。
Keyboard 键盘事件
键盘事件,比较复杂,除了普通键外,还有特殊键,快捷键,控制键等,如果是:
- 快捷键:NSApp会转发事件到相应NSWindow的控件或菜单,执行相应的操作;
- 控制键:转到到keyWindow,同不同的控件负责事件的响应;
- 普通键:由使用sendEvent()转发到keyWindow的First Responders对象视图,如果视图是文本类型,是执行insertText()方法,在文本控件中显示;
快捷键
快捷键,默认为 command+key
组合,其中command
为快捷键,下面代码是快捷键的判断,判断是否同时按下了command+j
,也可用普通的keyDown实现。
class PerformKeyButton: NSButton {
override func performKeyEquivalent(with event: NSEvent) -> Bool {
let characters = event.characters
if characters == "j" {
NSApp.terminate(self)
return true
}
return false
}
}
控制键
控制键包括:Tab、Shift、Space、Arrow、Option等,这些键用来在当前活动的Window内切换选择不同的控件或者模拟执行鼠标按下时的操作。
class KeyImageView: NSImageView {
override func keyDown(with event: NSEvent) {
NSLog("keyDown \(event.characters) modifierFlags = \(event.modifierFlags)")
//单个功能键按下判断
if event.modifierFlags.contains(NSEvent.ModifierFlags.command) {
NSLog("modifierFlags command")
}
if event.modifierFlags.contains(NSEvent.ModifierFlags.shift) {
NSLog("modifierFlags shift")
}
if event.modifierFlags.contains(NSEvent.ModifierFlags.control) {
NSLog("modifierFlags shift")
}
//组合按键判断,是否同时按下
if event.modifierFlags.contains(NSEvent.ModifierFlags.command) && event.modifierFlags.contains(NSEvent.ModifierFlags.shift) {
}
//回车
if event.keyCode == 36 {
NSLog("return key pressed!")
}
//空格
if event.keyCode == 49 {
NSLog("space key pressed!")
}
//上箭头
if event.keyCode == 126 {
NSLog("Up Arrow key pressed!")
}
//下箭头
if event.keyCode == 125 {
NSLog("Down Arrow key pressed!")
}
//左箭头
if event.keyCode == 123 {
NSLog("Left Arrow key pressed!")
}
//右箭头
if event.keyCode == 124 {
NSLog("Right Arrow key pressed!")
}
super.keyDown(with: event)
}
}
在Mac OS操作系统中默认提供了一些组合键,这些可以直接使用。也可以修改成自定义的组合键,配置文件存放在下边的路径中,注:不同版本的 OS X 路径不太一样,查下官方文档即可:
/System/Library/Frameworks/AppKit.framework/Versions/C/Resources/StandardKeyBinding.dict
文字输入
文字输入理论上只对TextField和Text View这两个控件有作用,文本输入分两种情况:
- 特殊键:比如delete, enter等由doCommandBySelector分发到对应的事件处理;
- 普通键:统一由insertText处理;
//要监控输入事件,首先打开开关
override var acceptsFirstResponder: Bool {
return true
}
普通键
普通的文本事件会触发keyDown()方法,在其内部会经过一个解析过程interpreteKeyEvents(),最后显示在相应的控件中。
override func keyDown(with event: NSEvent) {
self.interpretKeyEvents([event])
}
特殊键
对于特殊键,需要在doCommand()方法中做综合判断特殊处理(也可以复写特定的子方法,下面第二个例子中代码)。
class TextView: NSTextView {
// override func doCommand(by selector: Selector) {
// 回车键
// if selector == #selector(NSResponder.insertNewline(_:)) {
// NSLog("insertNewline")
// }
// }
override var acceptsFirstResponder: Bool {
return true
}
}
下面代码是模拟的一个文本输入框
class CustomTextView: NSView {
var string:NSMutableString = NSMutableString()
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
self.wantsLayer = true
self.layer?.borderWidth = 1
self.layer?.borderColor = NSColor.gray.cgColor
if string.length > 0 {
string.draw(in: dirtyRect, withAttributes: [:])
}
}
override func keyDown(with event: NSEvent) {
self.interpretKeyEvents([event])
}
override func insertText(_ insertString: Any) {
string.append(insertString as! String)
NSLog("insertString \(string)");
self.needsDisplay = true
}
override func moveUp(_ sender: Any?) {
NSLog("moveUp")
}
override func moveDown(_ sender: Any?) {
NSLog("moveDown")
}
override func moveLeft(_ sender: Any?) {
NSLog("moveLeft")
}
override func moveRight(_ sender: Any?) {
NSLog("moveRight")
}
override func insertNewline(_ sender: Any?) {
NSLog("insertNewline")
}
override func deleteBackward(_ sender: Any?) {
NSLog("deleteBackward")
}
override var acceptsFirstResponder: Bool {
return true
}
// override var canBecomeKeyView: Bool {
// return false
// }
//
// override var acceptsFirstResponder: Bool {
// return true
// }
//
//
// override func becomeFirstResponder() -> Bool {
//
// return true
// }
//
// override func resignFirstResponder() -> Bool {
// return true
// }
}
Action 消息事件
普通的事件在控件内部处理,比如keyDown这类事件。而Action消息事件处理一般是在target内部定义的,如NSControl, NSMenu等控件都是以Action消息形式响应事件的。
Action 消息是一种特殊的系统事件,它是通过NSapp的sendAction()
方法转发的,此方法有两个重要参数:
- action(sel):事件响应方法,类型为SEL对象;
- target(id):事件关联的对象,比如controller或其它;
- sender(id):事件发出者对象,一般是当前的控件;
在ViewController中处理事件
ViewController继承了NSResponser,对于普通事件可以直接处理,而复杂消息则需要先调用window.makeFirstResponder()成为第一事件响应者才能再处理。
比如在ViewController扩展以下内容:
extension ViewController: NSTextFieldDelegate,NSTextViewDelegate {
//MARK: NSTextFieldDelegate
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
NSLog("textField Enter key pressed!")
return true
}
if commandSelector == #selector(NSResponder.deleteBackward(_:)) {
NSLog("textField delete key pressed!")
return true
}
return false
}
//MARK: NSTextViewDelegate
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
NSLog("textView Enter key pressed!")
return true
}
if commandSelector == #selector(NSResponder.deleteBackward(_:)) {
NSLog("textView delete key pressed!")
return true
}
if commandSelector == #selector(NSResponder.insertTab(_:)) {
NSLog("textView tab key pressed!")
return true
}
return false
}
}