概要
- 「Mac でスクリーンショットを撮る」のような、画面上を選択しその範囲のスクリーンショットを取るような実装を行う
GitHub
参考
-
Kyome22/ScanTextFromImage
- 主に参考にしています
-
[iOS] frameとboundsの違いを理解する - Qiita
- frameが今回は対象
-
How to set a custom cursor in an SKScene? (XCode, Swift4, Spritekit)
- マウスカーソルの座標のNSTextFieldを今回はNSPanel側に表示しているが、カーソルの画像として表示させる方法も考えられる
-
CGWindowListCreateImage のオプション
- スクリーンショットの影をつけるオプション
- Programmatically Screenshot | Swift 3, macOS
Ask Question - Swift:あるNSWindowより下層領域のスクリーンショットを取得 - Qiita
実装
方針
- 前提として、複数のスクリーンがある状態(マルチディスプレイ)でも動作できるように考える
- まず各スクリーン上に全体を覆うような
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を閉じる
-
esc
を押下した際に、上記のScreenshotPanel
を全て閉じるため、下記プロパティを持たせておく
private var monitors = [Any?]() // keyDownの監視用
private var panels = [ScreenshotPanel]() // escが押された際にこれらを閉じる
カーソルの表示変更
- またスクリーンショット開始時のカーソルには
NSCursor.crosshair.set()
を使用する- 参考: NSCursor
- カーソルが切り替わるトリガは正確に把握していないけれど、
ScreenshotPanel
側でもマウスのアクションを受け取る際に随時呼び出すようにするといい感じに。 - 残念ながら、下記のMac標準のスクリーンショット時のカーソルは用意されてない
選択領域から画像の作成
- 下記で取得できる
selectedRect
は、各スクリーンの左下を原点としたときのNSRect
である- つまり
origin=NSPoint.zero
で固定としている
- つまり
extension ViewController: ScreenshotPanelDelegate {
func screenshotPanel(_ screenshotPanel: ScreenshotPanel, didSelectRect selectedRect: NSRect) {
- 各画面の全体のスクリーンショット画像作成関しては、Kyomeさんの記事を参照
- 座標の変換がややこしいので下記を参考にしつつ補正する
-
Swift: macOSでの座標系のややこしい話
- macOSでは左下が原点
- スクリーンショットの画像作成時にy軸が反転させる必要がある
- (旧) Cocoaの日々: NSScreen座標系 から CGWindow座標系へ
// 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
座標の扱い
- 下記2つの座標を情報として扱う(参考: Swift: macOSでの座標系のややこしい話)
- メイン画面の左下を原点としたときの座標
- パネルの画面の左下を原点としたときの座標
// メイン画面の左下を原点としたときの座標
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標準の範囲指定時のカーソル
- 下: 今回実装したもの
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
}()