LoginSignup
5
3

More than 1 year has passed since last update.

選択範囲のスクリーンショットを取得するmacOSアプリの作成

Last updated at Posted at 2021-10-16

概要

Oct-16-2021 22-32-29

GitHub

参考

実装

方針

  • 前提として、複数のスクリーンがある状態(マルチディスプレイ)でも動作できるように考える
  • まず各スクリーン上に全体を覆うようなScreenshotPanel(NSPanel)を最前面に配置する
    • 下図では分かりやすい用に青く着色している
  • ScreenshotPanel(NSPanel)上でマウスで領域を選択し取得できるようにする
  • 選択した範囲に沿うようにScreenshotView(NSView)を配置・変形させる

ViewController.swift

ScreenshotPanelの表示

  • 下記の通りScreenshotPanelを各スクリーン上に表示する
@IBAction func selectRect(_ sender: Any?) {
    guard panels.isEmpty else {
        return
    }

    NSCursor.crosshair.set()
    NSApp.activate(ignoringOtherApps: true)
    for (i, screen) in NSScreen.screens.enumerated() {
        let panel = ScreenshotPanel(screen.frame)
        panel.screenshotPanelDelegate = self
        panel.name = "Screen_\(i)"
        panels.append(panel)
        panel.orderFrontRegardless()
    }

    setupMonitors()
}

esc押下時にScreenshotPanelを閉じる

private var monitors = [Any?]()  // keyDownの監視用
private var panels = [ScreenshotPanel]()  // escが押された際にこれらを閉じる

カーソルの表示変更

  • またスクリーンショット開始時のカーソルにはNSCursor.crosshair.set()を使用する
  • カーソルが切り替わるトリガは正確に把握していないけれど、ScreenshotPanel側でもマウスのアクションを受け取る際に随時呼び出すようにするといい感じに。
  • 残念ながら、下記のMac標準のスクリーンショット時のカーソルは用意されてない

選択領域から画像の作成

  • 下記で取得できるselectedRectは、各スクリーンの左下を原点としたときのNSRectである
    • つまりorigin=NSPoint.zeroで固定としている
extension ViewController: ScreenshotPanelDelegate {

    func screenshotPanel(_ screenshotPanel: ScreenshotPanel, didSelectRect selectedRect: NSRect) {
// NSScreen座標系からCGWindow座標系に変換
let selectedRectInCGImage = NSRect(x: selectedRect.origin.x,
                                   y: screenshotPanel.frame.height - selectedRect.origin.y - selectedRect.height,
                                   width: selectedRect.width,
                                   height: selectedRect.height)

ScreenshotPanel.swift

座標の扱い

// メイン画面の左下を原点としたときの座標
private var pointInScreen: NSPoint {
    return NSEvent.mouseLocation
}

// パネルの画面の左下を原点としたときの座標
private var pointInPanel: NSPoint {
    return NSEvent.mouseLocation - self.frame.origin
}
  • 今回pointInScreenはカーソルがScreenshotPanel上にあるかを判断するためだけに使用する
private var containsCursorInScreen: Bool {
    return self.frame.contains(pointInScreen)
}
  • 基本的に座標はScreenshotPanel上で考えるためpointInPanelを扱うことになる
  • 絶対的な座標はScreenshotPanelから見ると関係ないことだし、またそうしたほうが扱いやすいため

マウスアクションの取得

  • 下記を使用してクリック・ドラッグ・移動時のマウスアクションを受け取る
  • マウスアクションに対して、選択領域の表示や座標のラベルの更新を行う
  • また領域が選択された場合は、delegateを使って選択領域をViewControllerへ通知を返している
// MARK: - Monitors

private func setupMonitors() {

    monitors.append(NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown, handler: { (event) -> NSEvent? in
        self.startPoint = self.pointInPanel
        self.endPoint = nil
        self.configurePointLabel()
        NSCursor.crosshair.set()
        return event
    }))

    monitors.append(NSEvent.addLocalMonitorForEvents(matching: .leftMouseDragged, handler: { (event) -> NSEvent? in
        self.endPoint = self.pointInPanel
        self.configurePointLabel()
        NSCursor.crosshair.set()
        return event
    }))

    monitors.append(NSEvent.addLocalMonitorForEvents(matching: .leftMouseUp, handler: { (event) -> NSEvent? in
        self.endPoint = self.pointInPanel
        if self.selectedRect == .zero {
            self.screenshotPanelDelegate?.screenshotPanelDidCancel(self)
        } else {
            self.screenshotPanelDelegate?.screenshotPanel(self, didSelectRect: self.selectedRect)
        }

        self.startPoint = nil
        self.endPoint = nil
        self.configurePointLabel()
        NSCursor.arrow.set()
        return event
    }))

    monitors.append(NSEvent.addLocalMonitorForEvents(matching: .mouseMoved, handler: { (event) -> NSEvent? in
        self.configurePointLabel()
        NSCursor.crosshair.set()
        return event
    }))
}

private func teardownMonitors() {
    for monitor in monitors {
        NSEvent.removeMonitor(monitor!)
    }
    monitors.removeAll()
}

CursorPointLabel.swift

概要

  • カーソルの右下に座標を表示するためのNSTextField
  • 座標に関して、Mac標準のスクリーンショット時のカーソルは各ディスプレイの左上を原点としているが、今回は左下を原点として実装している

座標の文字属性

  • 標準のスクリーンショット時のカーソルを参考に、座標の文字の属性を設定する
    • 黒い画面でも視認するよう、右下に向かって白い影をつけているのが分かる
  • 上: macOS標準の範囲指定時のカーソル
  • 下: 今回実装したもの

image

image

private var textAttributes: [NSAttributedString.Key : Any] = {
    var textAttributes: [NSAttributedString.Key : Any] = [
        .foregroundColor : NSColor.black,
    ]

    let myShadow = NSShadow()
    myShadow.shadowColor = NSColor.white
    myShadow.shadowBlurRadius = 1
    myShadow.shadowOffset = NSSize(width: 1, height: 1)
    textAttributes[.shadow] = myShadow

    return textAttributes
}()

座標のフォーマット

private var cunsomFormatter: NumberFormatter = {
    let formatter = NumberFormatter()
    formatter.numberStyle = .decimal
    formatter.groupingSeparator = "," // 区切り文字を指定
    formatter.groupingSize = 3 // 何桁ごとに区切り文字を入れるか指定

    return formatter
}()
5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3