NSWindow,一些很討厭的

提到 NSWindow 跟其他平台上面处理 Window 不一样的地方,现在来讲些一直以来我觉得 NSWindow 讨厌的地方。

首先,在 Cocoa 对 Window 的设计中,有个你光看名字实在看不懂什么意思,不看文件不可能猜得到的属性,叫做 Key Window-你可以决定一个 Window 是不是 Key Window,也可以询问目前的 Window 是不是 Key Window。

※ Key Window 与 Main Window

Key Window 是什么意思呢?从字面来看,好像是什么关键 Window,一个 Window 作为关键是什么意思?或是拿来当做钥匙的 Window 是什么意思?

Key Window 当中的 Key 完全不是指这个,而是正在负责处理键盘事件的 Window,比方说,你有一个应用程式有一个 Window,里头有的文字框,你正在这个文字框里头打字,这个 Window 就是 Key Window。

跟 Key Window 相关的另外一个属性叫做 Main Window,Main Window 通常是 Key Window,但是 Key Window 不见得是 Main Window。

Screen shot 2010-08-14 at 12.26.33 AM

我们来用 Safari 浏览器解释这件事情-在 Safari 里头,我们可以产生很多个浏览器视窗浏览网页,但是在浏览器里头想要打一篇文章的时候,突然发现不知道怎么打,于是我们叫出了字元面板,然后把输入焦点移动到了字元面板下方的搜寻框里头。这时候,字元面板就是我们的 Key Window,后面其中一个浏览器视窗则是 Main Window。

这两个属性决定了事件的传递:当使用者在键盘上面按了一个键,目的自然是想要在字元面板的搜寻框里头打字,所以我们要把这个事件送到字元面板上,我们就该把键盘事件分派到 Key Window;但如果是其他的操作行为,像是我们从书签选单上面选了一个书签,自然不会是由字元面板处理,而是当前的浏览器视窗开启书签,那就要送到 Main Window 上。

要让一个 Window 变成 Main Window 或 Key Window,就是透过 NSWindow 的 makeMainWindow 与 makeKeyWindow 这两个 method。于是你就可以看到官方文件里头让人想要骂人的地方-打开 Xcode 里头附的开发者文件,可以看到 NSWindow 部分中, 关于这两个 method 的解释:

makeKeyWindow
Makes the window the key window..

makeMainWindow
Makes the window the main window.

没什么问题。好,在 iOS 里头,有一个可以对应到 NSWindow 的物件,叫做 UIWindow,UIWindow 没有 makeMainWindow,只有 makeKeyWindow 以及 makeKeyAndVisible(就是你开始学写 iPhone 应用程式的第一课中,在 applicationDidFinishLaunching: 里头呼叫,让应用程式 Window 显示出来的 method),>文件里头则是这么说:

makeKeyWindow
Makes the receiver the main window.

喂喂喂…。

※ Cocoa 的 Events

这后面牵涉到 Cocoa 怎么传递事件,作业系统收到键盘或滑鼠事件之后,首先决定要由哪个应用程式负责,可能是交由目前使用者正在使用的应用程式,也可能是某个常驻应用程式,应用程式(NSApplication)收到事件后,就会透过 sendEvent: 把事件传递给应该处理的 Window,这就是 Key Window 与 Main Window 派上用场的时候了,接着,NSWindow 继续透过 sendEvent:,把事件传递给应该负责处理的 UI 元件。

同一个 Window 上面,可能有很多的 UI 元件,可能有一堆按钮、一堆文字框,NSWindow 应该把事件传递给哪一个呢?当然是使用者正在用的那个。

所谓的「正在用」,在 Cocoa Framework 里,称之为 First Responder。如果你写过 iPhone 可能就知道,想要让某个文字框可以不用等到使用者点选,就取得输入焦点、浮出萤幕键盘,就是让这个文字框变成 First Responder,像是呼叫:

[textfield becomeFirstResponder];
在 Mac OS X 里头,我们则是要让 NSWindow 决定他上面的哪个元件应该变成 First Responder:

[[textfield window] makeFirstResponder:textfield];
岔个题-在设定 target/action 的时候,如果把 target 设定成 nil 的话,就相当于呼叫 First Responder。在你的第一堂 iPhone 或 Mac 开发课程的讲义中,你学到在 Interface Builder 里头拉一个按钮出来,然后按着 Ctrl 按键,用滑鼠拉出一条连接线连到你的 controller 物件的某个 IBAction 上,对你的按钮来说,这个 controller 就是 target,指定的 IBAction 就是 action。

在 iPhone 上如果要用程式完成这样的连接,我们会呼叫 UIButton 的 addTarget:action:forControlEvents:,在 Mac 上面则是呼叫 NSButton 的 setTarget: 与 setAction:。你可以在这边把传入的 target 设成 nil,效果也就相当于,在 Interface Builder 里头,把连接线连到一个叫做 First Responder 的图示上,图示是一个红色的箱子。

在 iPhone 上可能不常这么做,但是在 Mac 上则会大量用到,比方说,我们在任何的文字框中,都可以用 Edit 选单里头的 Copy 指令复制文字内容。代表 Copy 指令的 NSMenuItem 自然不可能连接到应用程式里头所有的文字框上,而是连到 nil、连到 First Responder 上,看到使用者在用哪个文字框,才去复制那个文字框的文字。

※ sendEvent:

NSApplication 与 NSWindow 其实已经帮你完成大部分的事件传递,那,到底什么时候会有必要自己处理 sendEvent:,还要搞清楚事件是被传到了 Key Window 还是 Main Window?

在漫长的人生中,你可能会遇到这样的状况-你在写一个媒体播放程式,可能是放影片或是放音乐,这个程式里头有好几个 Window,有的 Window 播放影片、有的播放音乐,你可以把某个影片档案从 Finder 拖到这个应用程式的 Dock Icon 上,就会开出一个新视窗,用新视窗播放影片。印象中 QuickTime 的 Tutorial 就是教你怎么写一份这样的东西。

前一篇我们提到,这样的应用程式就是 document-based application,所以你产生了一个继承自 NSDocument 的新文件,这个 NSDocument 所管理的 Window 中有一个 QTMovieView,你直接把 NSDocument 读到的 fileURL 丢给 movie view 产生 QTMovie,可以播放影片了。你现在想要让软体操控变得更容易一点。

首先你想到的是在萤幕画面最上方的 menu bar 里头,增加一个选单,里头有要求影片播放或暂停的选项,还可以有一些透过改变 Window 大小来缩放影片的功能,你甚至在 Interface Builder 里头,把选单都加上了键盘快速键,像是用 cmd + 2 就可以变成两倍大小。这时候我们都用不到 sendEvent:,但是,因为我们有很多个 Window、很多个影片,播放或暂停等命令不会送到固定的地方,所以要让 First Responder 处理,我们把 target 都设成 nil。

我们打算用 play: 实作播放,用 pause: 实作暂停,这些 method 我们都放在我们的 NSDocument 子类别里。如此一来,在 Main Window 里头没有任何一个占据输入焦点的 UI 物件可以处理 play: 与 pause: 时,就会把事件送到 NSDocument 上。Xcode 提供的 template 里头,很多预设的 NSMenuItem 其实都把 action 送到 NSDocument 上,像,用 cmd + s 储存档案呼叫 NSDocument 的 saveDocument: ,还有列印等。

然后就遇到了问题-在 Mac 的键盘上,有一组按键叫做 Media Keys,在我的 MBP 与 MB 上是放在 F7 – F9 的位置,平常可以用来控制 iTunes 的播放、下一首上一首以及音量,你觉得如果是在你的应用程式中,这些按钮应该是用来控制你的影片用的。可是,NSApplication 根本就不帮你处理这些按键,收到之后直接无视。

在 iOS 上面其实也有相同的状况。iPad 可以透过蓝芽连接无线键盘,iOS 4.0 之后,iPhone 也可以,如果使用者买了一组苹果的蓝芽键盘连接这些装置,这些装置也都会收到这些按键,甚至耳机线控也都是送出一样的事件。

iOS 4.0 公开了这部份 API,找个地方实作 remoteControlReceivedWithEvent: 就好,至于 Mac 呢,则要看 Rogue Amoeba 前几年的文章:Apple Keyboard Media Key Event Handling。

这篇文章首先就告诉你,要拦截这些事件,就是要透过 subclass NSApplication,改写 sendEvent:,遇到 NX_KEYTYPE_PLAY (代表按下 play 键)等事件,就另外处理,不然就用 super 的实作。知道了是这些特别事件,在我们这个个案中,有几个方向可以选择:

一,我们可以把 NSEvent 再送给 Main Window,叫 Main Window 继续用 sendEvent: 处理。在这个例子里头这么做顶麻烦就是了,因为按下播放键的行为就是要播放影片,所以直接找到哪个是我们要的 NSDocument 物件,呼叫我们实作的 play:,会轻松取多。于是-

二,去找到底哪个 NSDocument 应该做事,也就是 Main Window 所属的 NSDocument 物件。有两种方法,1. 呼叫 [[[NSApp mainWindow] windowController] document]、2. 呼叫 [[NSDocumentController sharedDocumentController] documentForWindow:[NSApp mainWindow]]。

posted on 2013-05-27 09:39  陌上有缘  阅读(1144)  评论(0编辑  收藏  举报