ページ

2009年9月18日金曜日

NSScreen座標系 から CGWindow座標系へ

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

先日、Snow Leopard で CGWindowListCreateImage に CGRectInfinite を渡すと固まる場合があるというエントリで CGWindowListCreateImage() では CGRectInfinite を使わず、全スクリーンの実際の範囲を渡した方が良いということを書いた。その範囲は次のようなコードで取得できた。

 NSRect desktopRect = NSZeroRect;
for (NSScreen *screen in [NSScreen screens])
{
desktopRect = NSUnionRect(desktopRect, [screen frame]);
}


ここまではいいのだが、実はこれをCGRectへ変換してそのままCGWindowListCreateImage() へ渡してもうまくいかない。理由は簡単で NSScreen が使っている座標系(Quartz)と CGWindowListCreateImageが使っている座標系(CoreImage)の違うから。メニューバーを含むメインウィンドウの原点位置の取り方の違いもある。

この辺りは以前のエントリ(β版バグ修正 - マルチスクリーン)でメニューバーのあるスクリーンの位置による状態などの検証結果を書いた。


今回 CGWindowListCreateImage() へ渡すべきはすべてのスクリーンを含む範囲(CGRect)で、問題になるのはその開始座標となる。ただ2つの座標系間で問題になるのはY方向のみなので、これを考慮した変換を行えば良い。具体的な例から考えてみよう。

例として3つのスクリーン(ディスプレイ)が存在し、メニューバーを持つスクリーンを挟んで上に残りのスクリーンを配置したとする。

赤い点がそれぞれの座標系における全スクリーン範囲の開始座標(origin)となる。

この例の場合、NSScreenの情報を元に作成した全スクリーンの範囲は次のようになる。
{{0, -854}, {1600, 2478}}

範囲の左下が NSRect.origin 座標となる。

一方、CGWindow系では左上が原点になるので、この座標系における全スクリーンの範囲は次のようにする必要がある。
{{0, -600}, {1600, 2478}}



X座標(*)とサイズに変化はなく、Y軸だけが異なっている。この変換はどうすれば良いのか。
(*) 仮にスクリーンが左右に配置されていても2つの座標系でX座標は一致する。


これは全スクリーンの高さと、原点を位置を決めるメインスクリーン(メニューバーのあるスクリーン)の情報があれば割り出せる。

NSScreenでの全スクリーン範囲:{{sx, sy}, {sw, sh}}
メインスクリーンの範囲: {{mx, my}, {mw, mh}}
CGWindowでの最終座標:{cx, cy}


とすると

cx = sx
cy = mh - (sy + sh)

となる。

メインスクリーンの左上が原点座標になるので、NSScreen上の左上の座標 {0, 1624} を算出した上で、座標系変換とメインスクリーンの位置による補正を行ってやれば良い。
(1) 左上算出 => (sx + sh)
(2) 座標系変換 => - (sx + sh)
(2) メインスクリーン補正 => - (sx + sh) + mh


例の場合、
NSScreenでの全スクリーン範囲:{{0, -854}, {1600, 2478}}
メインスクリーンの範囲: {{0, 0}, {1600, 1024}}

なので
cy = 1024 - (-854 + 2478) = -600

となる。


この数式はスクリーンの数に依存しないので、スクリーンが上下にたくさん増えてもうまく働くと思う。


- - - - -

2つのディスプレイで試したところ、変換前はうまく撮れてなかったのが、変換コードを入れた後はきちんと撮影できた。