ひtoりgoと

NSView でくっきりラインを引く

macOS 開発

アプリケーションを作っていると目盛りを描画する機会があるはず。作り方なんて考えなくてもラインを縦横に並べるだけで描くことができる、描画の中でも基本中の基本。

でもラインって意外にデリケートで、気をつけないとスクリーンのピクセルの格子と合わずアンチエイリアスでぼやけてしまう。普通にくっきりしたラインを引くだけでも意外に難しいのだ。それに、せっかくなら Retina ディスプレイの実力も引き出したい...

そんなビューの簡単なサンプルを作ったので参考にどうぞ。Retina ディスプレイでもそうでなくても、くっきり 1 px のラインが表示されるはず。

Result
import Cocoa

class MemoriView : NSView
{
    override init(frame: NSRect)
    {
        super.init(frame: frame)
        wantsLayer = true
    }
    required init?(coder: NSCoder)
    {
        super.init(coder: coder)
        wantsLayer = true
    }

    override var isFlipped: Bool
    {
        return true
    }

    //MARK: - Properties

    var size_mm = NSSize(width: 100, height: 100)
    var memoriThickness_pt: CGFloat
    {
        let backingScaleFactor = max(1.0, (layer?.contentsScale ?? window?.backingScaleFactor ?? 1.0))
        return (1.0 / backingScaleFactor)
    }
    var memoriInterval_pt: CGFloat = 10.0

    //MARK: - Geometry Conversion

    var mmToPt: CGAffineTransform
    {
        let bounds = self.bounds
        return CGAffineTransform(scaleX: (bounds.width / size_mm.width), y: (bounds.height / size_mm.height))
    }

    //MARK: - Rects

    func rectForMemori(at location_mm: CGFloat, vertical: Bool) -> CGRect
    {
        var memoriRect_mm = NSRect(); do {
            if vertical {
                memoriRect_mm.size.width = size_mm.width
                memoriRect_mm.size.height = 0
                memoriRect_mm.origin.y = location_mm
            } else {
                memoriRect_mm.size.height = size_mm.height
                memoriRect_mm.size.width = 0
                memoriRect_mm.origin.x = location_mm
            }
        }
        var memoriRect_pt = memoriRect_mm.applying(mmToPt); do {
            if vertical {
                memoriRect_pt.size.height = memoriThickness_pt
                memoriRect_pt.origin.y -= (memoriRect_pt.size.height * 0.5)
            } else {
                memoriRect_pt.size.width = memoriThickness_pt
                memoriRect_pt.origin.x -= (memoriRect_pt.size.width * 0.5)
            }
        }
        return centerScanRect(memoriRect_pt)
    }

    //MARK: - Drawing

    override func draw(_ dirtyRect: NSRect)
    {
        let bounds = self.bounds

        NSColor.white.setFill()
        bounds.fill()

        NSColor.red.setFill()
        for memori_mm in stride(from: 0.0, to: size_mm.width, by: memoriInterval_pt) {
            if (memori_mm > 0.0) {
                let memoriRect = rectForMemori(at: memori_mm, vertical: false)
                memoriRect.fill()
            }
        }
        for memori_mm in stride(from: 0.0, to: size_mm.height, by: memoriInterval_pt) {
            if (memori_mm > 0.0) {
                let memoriRect = rectForMemori(at: memori_mm, vertical: true)
                memoriRect.fill()
            }
        }
    }
}

strokeLine ではなく fillRect

このサンプルでは一つ一つのラインを Rect として描画している。個人的にはその方が調整しやすい気がする。線幅の厚みでビューの幅(or 高さ)いっぱいに伸ばした Rect を作ったあと、これをピクセルの格子に合わせるだけだ。

-centerScanRect: を使う

ただしその工程が重要、誰もが考えるように NSIntegralRect() とか CGRectIntegral() で単純に整数化をすれば確かにくっきりするのだが、それではせっかく 200% HiDPI であっても 2 px(1 pt)ずつの精度でしか調整されず、Retina ディスプレイを活かしきれない。目盛りなんてできるだけ均等に並んでいた方が気持ちいいのだからここはこだわるべき。

AppKit の長い歴史を負の遺産だと思っている人は甘い。Retina ディスプレイなんて登場するよりもはるか昔、OS X 10.0 の頃にはすでにこのための API が用意されているのだ。-[NSView centerScanRect:] である。これに Rect を渡せばスケーリングを考慮してピクセル格子にぴったり合った Rect に変換してくれる。

例えば

NSRect(x: 10.3, y: 10.3, width: 10.3, height: 10.3)

という中途半端な Rect が存在するとき、非 HiDPI(100%)の場合は

NSRect(x: 10, y: 10, width: 10, height: 10)

Retina ディスプレイ(200% HiDPI)の場合は

NSRect(x: 10.5, y: 10.0, width: 10.5, height: 10.5)

に変換される。これを使用しておけばたとえ Mac の Retina ディスプレイが進化して 300% HiDPI が使われるようになっても最適な結果を得られるはずだ。

ディスプレイの設定に注意

上記のサンプルを動かしてもくっきり表示されない? そんな人はディスプレイの設定をチェック。現在の MacBook や MacBook Pro ではそもそもピクセル等倍で表示されず、より詰め込んで作業領域を確保した設定がデフォルトになっているのだ。

確認用に変更する場合、例えば MacBook (2016) の場合はディスプレイ解像度が 2304 × 1440 px であり、ここに HiDPI 200% のものをピクセル等倍で表示するには縦横それぞれ 2 で割った作業領域、つまり 1152 × 720 pt のものを選択する必要がある。

Share

リンクも共有もお気軽に。記事を書くモチベーションの向上に役立てます。

© 2005-2021 zumuya