cockscomblog?

cockscomb on hatena blog

NSWindowのタイトルバーをカスタマイズする

人類の多年の夢であったNSWindowのタイトルバーのカスタマイズであるが、YosemiteになってついにオフィシャルなAPIが実装された。


YosemiteではApple製のアプリケーションほとんどにおいてデザインが一新され、かつてOS Xを特徴付けていたツルッとしたテクスチャは取り払われた。そしてウインドウのタイトルバーまでもが変わった。

f:id:cockscomb:20150208004605p:plain
タイトルバー

ウインドウのタイトルバーは、OS XにおいてGUIアプリケーションの要である。タイトルバー自体がウインドウを移動するためのノブであり、左にはウインドウを操作するための赤・黄・青のコントロールが、中央にはウインドウのタイトルが表示される。ときにはツールバーと見かけ上くっついたりするが、いずれにしてもウインドウの中心はタイトルバーである。

f:id:cockscomb:20150208005228p:plain
Safariのタイトルバーにはツールバーが表示されている

そのタイトルバーをカスタマイズしたい、これは全人類が生まれながらに持っている当然の欲求である。そして、しかし、Appleは長年にわたってそういったAPIを提供してこなかった。ウインドウをウインドウたらしめるタイトルバーを、心ないデベロッパーが欲望のままに毀損することを許さないというように、禁断の果実を決して手放そうとはしなかった。それがYosemiteになって、Apple製のアプリケーションでさえ、一般的なタイトルバーのルールを破壊した。いまやタイトルバーには信号機のようなコントロールを残して、タイトルの代わりにツールバーが表示されたり、あるいはウインドウのコンテンツがそのまま表示されるようになった。これはいまやAppleだけの特権ではなく、ついにAPIが提供されたのである。

f:id:cockscomb:20150208005317p:plain
リマインダーのタイトルバーはコンテンツと重なる

タイトルを非表示にする

OS X 10.10 YosemiteからNSWindowに新しく追加されたプロパティvar titleVisibility: NSWindowTitleVisibilityを使うと、ウインドウのタイトルを非表示にできる。enum NSWindowTitleVisibilityは以下のように二つの値からなる。

enum NSWindowTitleVisibility : Int {
    case Visible
    case Hidden
}

デフォルトは.Visibleであるが、ここでヘッダから.Hiddenのコメントを引用する。

The always hidden mode hides the title and moves the toolbar up into the area previously occupied by the title.

NSToolbarがある場合にはタイトルがあった場所に移動するようだ。これによって、例えばSafariやカレンダーのように、タイトルバーの高さが少し高く、信号機のようなコントロールのすぐ右にツールバーがあるようなウインドウが作れる。NSWindowNSToolbarを設定して、あとは以下のようなコードをNSWindowControllerfunc windowDidLoad()などに実装する。

window?.titleVisibility = .Hidden

f:id:cockscomb:20150208005920p:plain
単にツールバーを置いたウインドウ

f:id:cockscomb:20150208005928p:plain
ツールバーを置いてtitleVisibility = .Hiddenとしたウインドウ

タイトルバーを装飾する

OS X 10.10 YosemiteではNSViewControllerが大幅に強化され、iOSUIViewControllerから多くの思想を受け継いだ。そして同時にNSViewControllerのサブクラスがいくつか追加された。その一つがNSTitlebarAccessoryViewControllerである。

NSTitlebarAccessoryViewControllerは、その名前が示す通り、タイトルバーを装飾するViewのControllerである。このViewControllerのviewプロパティが返すViewが、NSWindowのタイトルバーに表示される。合わせてNSWindowにも以下のようなプロパティやメソッドが追加されている。

var titlebarAccessoryViewControllers: [AnyObject]
func addTitlebarAccessoryViewController(childViewController: NSTitlebarAccessoryViewController)
func insertTitlebarAccessoryViewController(childViewController: NSTitlebarAccessoryViewController, atIndex index: Int)
func removeTitlebarAccessoryViewControllerAtIndex(index: Int)

NSWindow複数NSTitlebarAccessoryViewControllerを順序付きで管理する。タイトルバーの中でどの部分に表示されるのかは、NSTitlebarAccessoryViewControllerの持つvar layoutAttribute: NSLayoutAttributeというプロパティで決定される。ただし現在の所NSLayoutAttributeのうち有効な値は.Bottom.Rightのみであることがヘッダに書かれている。もちろんそれぞれに対応する位置は、タイトルバーの下と右である。

実際に表示するには以下のようなコードを、例えばNSWindowControllerfunc windowDidLoad()に実装する。NSTitlebarAccessoryViewControllerインスタンスはStoryboardから生成している。

let accessory = storyboard?.instantiateControllerWithIdentifier("TitlebarAccessory") as NSTitlebarAccessoryViewController
accessory.layoutAttribute = .Right
window?.addTitlebarAccessoryViewController(accessory)

f:id:cockscomb:20150208010129p:plain
layoutAttribute = .BottomNSTitlebarAccessoryViewControllerを設定したウインドウ

f:id:cockscomb:20150208010116p:plain
layoutAttribute = .RightNSTitlebarAccessoryViewControllerを設定したウインドウ

コンテンツをタイトルバーの領域まで拡げる

タイトルバーの位置までウインドウのコンテンツを表示するために、新たにvar titlebarAppearsTransparent: Boolプロパティと、NSFullSizeContentViewWindowMask定数が用意された。

titlebarAppearsTransparentはタイトルバーの背景を透明にするかどうかを表す。NSFullSizeContentViewWindowMaskは、NSWindowvar styleMask: Intプロパティに利用する定数で、タイトルバーの領域までをコンテンツの領域とすることを表現するものである。

NSFullSizeContentViewWindowMaskNSScrollViewの組み合わせによって、タイトルバーの下にコンテンツが透けるような表現が可能になる。またNSFullSizeContentViewWindowMaskを設定した上でtitlebarAppearsTransparentを真にすると、信号のようなコントロールだけのウインドウが作れるのだ。

window?.titleVisibility = .Hidden
window?.styleMask |= NSFullSizeContentViewWindowMask
window?.titlebarAppearsTransparent = true

f:id:cockscomb:20150208010259p:plain
タイトルバーの部分までNSSplitViewのコンテンツを表示させるようにしたウインドウ

伝統的手法

有史以来、NSWindowのタイトルバーをカスタマイズすると言えば大きく二つの方法があり、ひとつはウインドウのコントロールまで完全に自作して自由に表示する方法、そしてもうひとつはwindow.contentView.superviewという風にNSWindowの実装に依存してビューの階層を辿る方法である。現実的には主に後者が利用されることが多かったのではないだろうか。(NSThemeFrameなどで検索するとそういった方法を指し示すページが今も見つかるだろう。)

しかしOS X 10.10 Yosemiteでは、このビューの階層を辿る方法を使っていると"NSWindow warning: adding an unknown subview:"というログが出力されるようになり、代わりにオフィシャルな方法が提供されることになった。このことはAppKitのリリースノートに記載されている。

まとめ

ここまでで、OS X 10.10 Yosemiteから追加されたAPIによって、柔軟にタイトルバーをカスタマイズできることがわかった。これらはもちろん組み合わせることもできる。例えばSafariでは、titlebarAppearsTransparent = trueとした上でlayoutAttribute = .BottomNSTitlebarAccessoryViewControllerを設定しているかもしれない。

f:id:cockscomb:20150208010722p:plain
SafariのようにtitlebarAppearsTransparent = trueかつlayoutAttribute = .BottomNSTitlebarAccessoryViewControllerを設定したウインドウ

また昨今噂されるUXKitでは、UXNavigationControllerなどUIKit由来の機能が存在するらしい。これらのNSWindowの新しいAPIを使って、例えばUINavigationBar相当の機能をタイトルバーに押し込めることもできるのではないだろうか。

まだ見ぬOS X 10.11への期待を膨らませつつ、読者諸氏もNSWindowのタイトルバーをカスタマイズしてみてほしい。こちらからは以上だ。


詳解 Swift

詳解 Swift