[macOS]ペンタブレットの筆圧とかを取得したい


そろそろマスウイベントでも扱うかと思って手始めに筆圧でも取得してやろうかとしたがいまいちわからない。調べたところ、ViewControllerとかでmouseDowunを定義してやる方法と、マウストラッキングというやつを使う方法があることが分かった。というわけで、ひとまずペンタブの座標や筆圧とかを表示するだけのアプリを作ることにした。まず手始めにStoryboardでテキストラベルを貼る。

ViewController
    @IBOutlet var _view: NSView!

    @IBOutlet weak var pressureIndicator: NSTextField!
    @IBOutlet weak var coordinateIndicator: NSTextField!
    @IBOutlet weak var rotateIndicator: NSTextField!
    @IBOutlet weak var tiliIndicator: NSTextField!

ViewControllerにmouseDragged(with:)等を実装する方法

んでもってViewControllerにmouseDraggedを書く。mouseナントカ系のメソッドはもともとNSResponderで定義されているやつで、NSViewはNSResponderを継承しているので使える。overrideを書かないとXcodeが怒る。

ViewController
 override func mouseDragged(with event: NSEvent) {
        pressureIndicator.stringValue = "筆圧:\(event.pressure)"
        coordinateIndicator.stringValue = "座標:\(event.locationInWindow)"
        rotateIndicator.stringValue = "回転:\(event.rotation)"
        tiliIndicator.stringValue = "傾き:\(event.tilt)"
        // "event"の中に必要な情報が入っているので適宜取り出す
    }

引数として貰ってきたevent変数の中にペンタブの各種情報が入っている。んで種類に応じて取り出してやるという格好になる。今回は全て文字列として表示しているだけなので型チェックなどは行っていない。
今回の場合ウィンドウが一つだけなのでViewController中に直接書いても問題なさそうだが、大量の設定ウィンドウが並んだ設計だと、書いてる間に「どのマウスダウンだよ」てなことになりそうである。MTKViewに対するMTKViewdelegateみたいな感じで、処理内容のグループに応じて処理をマルナゲできるような仕組みを作れば良さそう。

さて動かすとかんな感じ。座標はウィンドウの左下隅を原点(0,0)としており、ドラッグしたままウィンドウの外にカーソルがはみ出るとマイナスになる場合がある。ちなみに、座標のところをevent.locationInWindowでなくNSEvent.mouseLocationとしてやると、ディスプレイ全体を対象にした座標が得られる。モニターの左下隅が(0,0)で、Retina 5Kモニタでも一番右上は5120にならず半分である。
ところで、実行結果を見るとなんか傾きだけDoubleで貰ってるように見える。(各項目ともウィンドウ端限界までテキストフィールドが伸びているので表示しきれないということでもなさそうである)ペンの傾きってそんな精度いる??まあ貰えるなら貰うけど使うときはFloatに変換してそうだなあ。

ここまでわりとうまくできたけど、例えば「線を引く前にペン軸の回転に沿ってブラシプレビューを表示したい」という時にはmouseDraggedは使えない。つまり、ペンタブが浮いている時に回転や傾きを検出するのは別の方法が必要になる。

NSTrackingAreaを使う方法

Using Tracking-Area Objects
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/TrackingAreaObjects/TrackingAreaObjects.html#//apple_ref/doc/uid/10000060i-CH8-SW1

NSTrackingAreaの初期化 - Qiita
https://qiita.com/toshi0383/items/90219268f4e263e92051

もともとmouseMovedというメソッドがあることはわかっていたが、なぜかこれが機能しない。どうやら上記のNSTrackingAreaオブジェクトというのを使わねばならないらしいんだが、下準備がややこやしい。

まずNSTrackingAreaの初期化の前に、オプション項目をまとめた配列を作っておき、それをイニシャライザに渡すことで生成ができる。ちなみにoption項目はコード補完は効かないのでドキュメントからコピペしてくる必要がある。もっと簡単にならんのかこれ。
んでもって必須の項目を書かないと、落ちはしないがエラーになって機能しない。これもっとどっか大きく書いた方がいいと思うよマジで。

func configure() {
    let options:NSTrackingArea.Options = [
        .mouseEnteredAndExited,
        .mouseMoved,
        .cursorUpdate,
        .activeInKeyWindow
    ]
    let trackingArea = NSTrackingArea(rect: _view.bounds, options: options, owner: self, userInfo: nil)
    _view.addTrackingArea(trackingArea)
    // _view というのは今回使っているViewControllerのview
}

override func viewDidLoad() {
      super.viewDidLoad()

      configure()  //viewDidLoadの中で上のセッティングを実行しNSTrackingAreaを生成

これで晴れてmouseMovedメソッドが機能するようになった。んでもってMovedメソッドだのDraggedメソッドだので毎度毎度pressureIndicator.stringValue = "筆圧:\(event.pressure)"とか書くのが非常にダルいのでひとまとめにした。まあ速度とかそんなに変わらんやろ。

func getPentabletProperty(by event: NSEvent) {
       pressureIndicator.stringValue = "筆圧:\(event.pressure)"
       coordinateIndicator.stringValue = "座標:\(event.locationInWindow)"
       rotateIndicator.stringValue = "回転:\(event.rotation)"
       tiliIndicator.stringValue = "傾き:\(event.tilt)"
   }
override func mouseMoved(with event: NSEvent) {
        getPentabletProperty(by: event) //貰ったeventをそのままマルナゲ
   }

mouseMovedがあるんだからもうmouseDraggedいらんやろと思って無効にしたらドラッグ中にだけ機能停止した。moveとdraggはハッキリ別のものということか。じゃないと「ペンで線を引く」時と「ペンを離した」時とでずっと同じ操作をしてしまうわな。

というわけで できたーーーーー!!