Shujima Blog

Apple製品,技術系の話をするブログ

macOS Cocoa Appでマウスの座標を受け取る,強制的に移動させる

環境

  • macOS 10.15.5
  • Xcode 10.2.1
  • Swift 5.0.1

この記事で作ったプロジェクトを前提にしています.

www.shujima.work

なんの変哲も無いCocoa Appにボタン,ラベル × 2,テキストフィールド × 2を配置したものです.

f:id:masa_flyu:20190707022151j:plain

なお,私はxcode使用2日目の初心者です.ツッコミどころがあればコメント,お問い合わせなどをお願いします.温かい目で見てください.

この記事でやることの説明

本記事で作成するアプリケーションは

  • マウスのカーソル位置をアプリ内で取得する
  • マウスを特定の座標に強制的に移動させる

ことができます.

アプリがフォアグラウンドになっている時のみ取得可能です.

これは,macOSがセキュリティのために設けている制約であり,常に取得するためには,異なるプログラムを追記するとともに,ユーザに許可を求める必要があります.

マウスの座標を読むプログラム

ViewController.swiftに以下4行+4行の2箇所を追記します

  • 以前の記事からきた方へ:追記にあたってstoryboard等での追加作業はありません.
  • この記事に直接飛んできた方へ:このプログラム全体をコピペするだけでは動きません.かならずstoryboardでのGUI要素配置,IBOutletの紐付け作業を行なってください.それ以外の設定などは行なっていないので,IBOutletなどがわかる人は以前の記事を参照する必要はありません.
class ViewController: NSViewController {
    @IBOutlet weak var textfield_x: NSTextField!
    @IBOutlet weak var textfield_y: NSTextField!
    @IBOutlet weak var button_move: NSButton!
    @IBOutlet weak var label_left: NSTextField!
    @IBOutlet weak var label_right: NSTextField!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Do any additional setup after loading the view.
        label_left.stringValue = "Hello World!"
        //↓ここから追記
        NSEvent.addLocalMonitorForEvents(matching: .mouseMoved){ (event) -> NSEvent in
            self.MouseMoved(with: event)
            return event
        }
        //↑ここまで追記
    }
    //↓ここから追記
    func MouseMoved(with event: NSEvent) {
        label_left.stringValue = "X = \( event.locationInWindow.x ) \r\n"
            + "Y = \( event.locationInWindow.y ) \r\n"
    }
    //↑ここまで追記

f:id:masa_flyu:20190707090557j:plain

アプリケーションがフォアグラウンドの間,左側のラベルにマウスの座標が表示されるようになりました.

説明

        NSEvent.addLocalMonitorForEvents(matching: .mouseMoved){ (event) -> NSEvent in
            self.MouseMoved(with: event)
            return event
        }

NSEventのaddLocalMonitorForEventsを用いて操作のイベントを取得します.今回は「.mouseMoved」を選択したので,マウスが動く度にイベントを取得できます.

変数eventを受け取り,それを自分で作ったイベント関数MouseMovedに渡します.

なぜかよくわかりませんが,実行が終わった後,イベント変数を戻り値で返さなければいけないようです.

マウスの座標を強制的に動かす

強制的に移動させるためにはCGDisplayMoveCursorToPoint()関数を使用します.

以下の1行を前の記事で作成したボタンのイベント関数button_move_clickedに追記します.

    @IBAction func button_move_clicked(_ sender: Any) {
        label_right.stringValue = "X = \( textfield_x.stringValue ) \r\n"
            + "Y = \( textfield_y.stringValue ) \r\n"
        //↓1行を追記
        CGDisplayMoveCursorToPoint(0,CGPoint(x: Double(textfield_x.floatValue), y: Double(textfield_y.floatValue) ) )

    }

ビルドして実行します.

f:id:masa_flyu:20190707100803g:plain

表示自体は変わらないもののテキストフィールドに座標を入力してボタンを押すとマウスカーソルがワープします.

説明

        CGDisplayMoveCursorToPoint(0,CGPoint(x: Double(textfield_x.floatValue), y: Double(textfield_y.floatValue) ) )

CGDisplayMoveCursorToPointの1つ目の引数はCGDirectDisplayIDを入力します.

ただし,ID以外の値を入力すると勝手にメインディスプレイを原点座標に取ってくれるので,0を入れています.

2つ目の引数は飛んでいく座標をCGPoint型で入力します.SwiftではCGPoint()で囲い,かつ各要素にx:y:というラベルを付けなければなりません.

以上作ったプログラムの全景です.

import Cocoa

class ViewController: NSViewController {
    @IBOutlet weak var textfield_x: NSTextField!
    @IBOutlet weak var textfield_y: NSTextField!
    @IBOutlet weak var button_move: NSButton!
    @IBOutlet weak var label_left: NSTextField!
    @IBOutlet weak var label_right: NSTextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Do any additional setup after loading the view.
        label_left.stringValue = "Hello World!"
        //マウスが動いた時にイベントを生成し,MouseMoved()関数を呼ぶように設定
        NSEvent.addLocalMonitorForEvents(matching: .mouseMoved){ (event) -> NSEvent in
            self.MouseMoved(with: event) //MouseMoved()関数を呼ぶ
            return event
        }
    }
    
    //マウスが動いた時に呼ばれるイベント関数
    func MouseMoved(with event: NSEvent) {
        //左のラベルにマウス座標を書き込み
        label_left.stringValue = "X = \( event.locationInWindow.x ) \r\n"
            + "Y = \( event.locationInWindow.y ) \r\n"
    }


    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }
    
    //ボタンが押されたときに呼ばれるイベント関数
    @IBAction func button_move_clicked(_ sender: Any) {
        //右のラベルにテキストフィールドの値を書き込み
        label_right.stringValue = "X = \( textfield_x.stringValue ) \r\n"
            + "Y = \( textfield_y.stringValue ) \r\n"
        //マウスを強制的に移動
        CGDisplayMoveCursorToPoint(0,CGPoint(x: Double(textfield_x.floatValue), y: Double(textfield_y.floatValue) ) )
    }
}

おまけ:マウスの座標を強制的に動かす(id考慮編)

さきほどのプログラムは原点がメインディスプレイになるというだけで,別のディスプレイもマイナスなどを用いて表現可能です.

一応別のディスプレイに対応したバージョンも載せておきます.

3箇所変更されています.

import Cocoa

class ViewController: NSViewController {
    @IBOutlet weak var textfield_x: NSTextField!
    @IBOutlet weak var textfield_y: NSTextField!
    @IBOutlet weak var button_move: NSButton!
    @IBOutlet weak var label_left: NSTextField!
    @IBOutlet weak var label_right: NSTextField!
    
    //↓2行追記
    var ids:[CGDirectDisplayID] = []
    var idcount:UInt32 = 0
    //↑2行追記
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Do any additional setup after loading the view.
        label_left.stringValue = "Hello World!"
        NSEvent.addLocalMonitorForEvents(matching: .mouseMoved){ (event) -> NSEvent in
            self.MouseMoved(with: event)
            return event
        }
        
        //↓7行追記
        CGGetOnlineDisplayList( 100, nil, &idcount )
        ids = [CGDirectDisplayID](repeating: 0, count: Int(idcount))
        label_right.stringValue = ""
        CGGetOnlineDisplayList( idcount, &ids, &idcount )
        for id in ids{
            label_right.stringValue += "id = \(id)\r\n"
        }
        //↑7行追記
    }
    
    func MouseMoved(with event: NSEvent) {
        label_left.stringValue = "X = \( event.locationInWindow.x ) \r\n"
            + "Y = \( event.locationInWindow.y ) \r\n"
    }


    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }
    
    @IBAction func button_move_clicked(_ sender: Any) {
        label_right.stringValue = "X = \( textfield_x.stringValue ) \r\n"
            + "Y = \( textfield_y.stringValue ) \r\n"
        //元々のCGDisplayMoveCursorToPoint()の1行を以下6行に書き換え,第1引数の違いに注意.
        if(idcount >= 1){
            CGDisplayMoveCursorToPoint(ids[1],CGPoint(x: Double(textfield_x.floatValue), y: Double(textfield_y.floatValue) ) )
        }
        else{
            CGDisplayMoveCursorToPoint(ids[0],CGPoint(x: Double(textfield_x.floatValue), y: Double(textfield_y.floatValue) ) )
        }
        //6行書き換え
    }
}

f:id:masa_flyu:20190707102712j:plain

実行するとディスプレイの台数分idが表示されます.

先ほどまでと異なり,2つ以上のディスプレイがある場合2つ目のディスプレイを原点に取ります.

当ブログをご利用いただく際には免責事項をお読みください。