kanizaのブログ

コンピュータ、ソフトウェア、映画、音楽関連や家族のことなど、思いついたことを書きます。

NSViewへのオーバーレイ表示

Cocoaについて勉強したメモ。僕はCocoa初級者なので間違ってるかもしれません。

はじめに

あるNSView(今回はPDFView)の上に絵を描きたいと思った。カーソルとか、そういう感じでオーバーレイ表示されるやつ。

すぐ思いつくのが次のような方法。

  • サブクラスを作って、drawRectをオーバーライドする
  • サブビューとして追加して描画する
  • NSViewを重ねあわせて配置して描画する

まず、PDFViewのような複雑なビューのdrawRectを単純にオーバーライドしてしまうのもいかがなものかと思われる(よくわからんけど、試してみても描画できなかった)。

同様の理由で、ザブビューを追加するのも気がひける。

あと、Cocoaでは親子関係にないNSViewを2つ重ねるのは推奨していないそうで、3つめの方法もよろしくなさそうだ(試したけどPDFをスクロールすると表示がおかしくなった)。

というわけで、なんか方法ないのかなーとCocoaのMLを探して見つけた「別の透明ウィンドウを重ねて表示して、そこに置いたNSViewに描画する」という方法をとった。

透明ウィンドウ

ウィンドウを透明にするには、RoundTransparentWindowサンプルにあるように、NSWindowのサブクラスでinitWithContentRectをオーバーライドしてやると良い。

- (id)initWithContentRect:(NSRect)contentRect styleMask:(unsigned int)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag 
{
    NSWindow* window = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
    [window setBackgroundColor: [NSColor clearColor]];
    [window setAlphaValue:1.0];
    [window setOpaque:NO];
    return window;
}

このウィンドウを、Interface Builder上でメインのウィンドウに重なるように設定して、常に上に表示するようにし、移動やリサイズに追従するようにすればいい。

子ウィンドウの設定

リサイズ以外、つまり「常に上に表示して、移動に追従する」ようにするには、ウィンドウを「子ウィンドウ」に設定してやればいい。引数で「常に上に表示」を指定できる。コードとしては次のような感じ。

[parentWindow addChildWindow: childWindow ordered: NSWindowAbove];

リサイズについては、親ウィンドウのリサイズ時にNSWindowのdelegateメソッドが呼ばれるので、そちらで子ウィンドウ側もいじってやればいい。

描画

子ウィンドウ側に置いたNSViewのdrawRectで好きな絵を描いてやればいい。マウスイベントとかは透けて見える親ウィンドウ側がちゃんと受けとってくれるので問題ない。重なった部分の親ウィンドウ側でスクロールしてもちゃんと描画されていた。

親ウィンドウのNSViewと関連する描画の際、NSView間で座標変換する場合には注意が必要。NSViewにある座標変換メソッドは、基本的に同じウィンドウ内にあるNSView間のものとなっているようだ。別ウィンドウのNSView間で座標を変換する場合は、いったんウィンドウ間での座標変換処理を入れる必要がある。

このへんのコードを書いていると、Cocoaの描画座標の基点が左下になってるのがわかりにくい。しかも全部じゃなくて、反転してるやつとかもあるのだな。前回のCocoa勉強会関西で沼田さんが文句を言っていた理由がよくわかった。

まとめ

  • NSViewにオーバーレイ表示をするには透明な子ウィンドウを設定してそちらに描画する手段がある。
  • 子ウィンドウ側の移動とリサイズを追従させるには、片方を子ウィンドウとして親に登録して、サイズをdelegateで同期させる。
  • 透明ウィンドウの背後に透けて見えるウィンドウは、ちゃんとイベントを受けとって処理してくれる。
  • Cocoaの左下原点の座標系は面倒。