Swift: macOSでの座標系のややこしい話

公開:2020/12/15
更新:2020/12/18
3 min読了の目安(約3000字TECH技術記事

はじめに

iOS は常に画面左上が原点ですが、macOS の世界では原点の位置がフレームワークによって異なります。主に3つの座標系が存在し、この記事ではそれらをNSScreen座標系CGWindow座標系CGImage座標系と呼称します。

基本認識

上で述べたように iOS の原点は基本的に左上ですが、macOS での基本的な原点は画面左下になります。

左:iOS、右:macOS

回転方向

iOS と macOS で同様のコードを書いてみて、回転方向について検証してみましょう。

iOS
import UIKit

class DrawView: UIView {
    override func draw(_ rect: CGRect) {
        let w = self.frame.width
        let h = self.frame.height
        let len = 0.5 * min(w, h)
        let center = CGPoint(x: 0.5 * w, y: 0.5 * h)

        let anchor1 = CGPoint(x: center.x + len * cos(0),
                             y: center.y + len * sin(0))
        let path1 = UIBezierPath()
        path1.move(to: center)
        path1.addLine(to: anchor1)
        path1.stroke()

        let anchor2 = CGPoint(x: center.x + len * cos(CGFloat.pi / 3.0),
                              y: center.y + len * sin(CGFloat.pi / 3.0))
        let path2 = UIBezierPath()
        path2.move(to: center)
        path2.addLine(to: anchor2)
        path2.stroke()
    }
}

実行結果

時計回りですね。

macOS
import Cocoa

class DrawView: NSView {

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)

        let w = self.frame.width
        let h = self.frame.height
        let len = 0.5 * min(w, h)
        let center = CGPoint(x: 0.5 * w, y: 0.5 * h)

        let anchor1 = CGPoint(x: center.x + len * cos(0),
                             y: center.y + len * sin(0))
        let path1 = NSBezierPath()
        path1.move(to: center)
        path1.line(to: anchor1)
        path1.stroke()

        let anchor2 = CGPoint(x: center.x + len * cos(CGFloat.pi / 3.0),
                              y: center.y + len * sin(CGFloat.pi / 3.0))
        let path2 = NSBezierPath()
        path2.move(to: center)
        path2.line(to: anchor2)
        path2.stroke()
    }
}

実験結果

反時計回りですね。

本題:macOSのややこしい座標系

Mac の場合ディスプレイを複数繋ぐことがあり得るため、メインのディスプレイ(Dockが置いてあるディスプレイのこと)を中枢として座標系が構成されるようです。

NSScreen座標系

メインのディスプレイの左下を原点とした座標系です。NSWindowNSViewなどのframeは基本的にこの座標系にしたがっています。メインのディスプレイよりも下のディスプレイに配置されたウィンドウのy座標はマイナスになるので、若干扱いづらいです。

NSScreen座標系でのマウスカーソルの位置の取得方法

let location: NSPoint = NSEvent.mouseLocation

CGWindow座標系

メインのディスプレイの左上を原点とした座標系です。一見iOSの座標系と同じように扱えて便利に思えますが、NSScreen座標系と混ざることがなかなか避けられないため、そうでもありません。CGWindow座標系はCGGetDisplaysWithPoint()CGWindowListCreateImage()など、スクリーンのキャプチャを取得する時に重要です。

CGWindow座標系でのマウスカーソルの位置の取得方法

let location: CGPoint = CGEvent(source: nil)!.location

CGImage座標系

メインのディスプレイとか関係なく、一枚の画像データに関する座標系で、左下を原点としています。ただ、ピクセルレベルでの情報取得を試みる場合、アクセス方法によっては左上原点になる場合があるのでこれもまた要注意です。

終わりに

とにかく、macOS では原点が安定しないので座標系を意識しながらプログラムすることがとても大切です。画面を画像や動画としてキャプチャするようなアプリを開発する場合は、複数ディスプレイを繋いだ状態でデバッグすることをお勧めします。