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]]。

One thought on “NSWindow,一些很討厭的

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.