[macOS][Swift4.1] すべてのウインドウをキャプチャして一覧表示する方法


Macアプリで他アプリも含めたすべてのウインドウをキャプチャして、スクリーンショット一覧を表示する方法を記載します。

サンプルコードは以下にアップしました。
https://github.com/atsushijike/AllWindows

  • Xcode 9.4.1
  • Swift 4.1

概要

他のアプリの NSWindowCGWindowRef などのウインドウオブジェクトを取得することはできませんが、 CGWindowListCopyWindowInfo(_:_:) を使用することで以下のようなウインドウの情報を取得することができます。

{
    kCGWindowAlpha = 1;
    kCGWindowBounds =     {
        Height = 546;
        Width = 1994;
        X = 52;
        Y = 478;
    };
    kCGWindowIsOnscreen = 1;
    kCGWindowLayer = 0;
    kCGWindowMemoryUsage = 1128;
    kCGWindowName = Code;
    kCGWindowNumber = 3190;
    kCGWindowOwnerName = Finder;
    kCGWindowOwnerPID = 567;
    kCGWindowSharingState = 1;
    kCGWindowStoreType = 1;
}

kCGWindowNumberキーから CGWindowListCreateImage(_:_:_:_:) を使用してウインドウキャプチャイメージを取得することができます。

ウインドウ一覧の取得

ウインドウIDとアプリ名、キャプチャイメージを持つ Window クラスを定義します。

Window.swift
class Window {
    fileprivate(set) var id: CGWindowID = 0
    fileprivate(set) var name: String!
    fileprivate(set) var image: NSImage!
   ...
}

CGWindowListCopyWindowInfo で取得した情報を元に Window を生成して配列に追加します。

AppDelegate.swift
class AppDelegate: NSObject, NSApplicationDelegate {
    ...
    private var windows: [Window] = []
    ...
    func reloadWindows() {
        windows.removeAll()
        if let windowInfos = CGWindowListCopyWindowInfo([.optionAll], 0) {
            for windowInfo in windowInfos as NSArray {
                if let info = windowInfo as? NSDictionary,
                    let window = Window(with: info) {
                    print("\(info)")
                    windows.append(window)
                }
            }
        }
        collectionView.reloadData()
    }
    ...
}

フルスクリーン時に取得すると同じスクリーンにあるウインドウが取得できないため、
option 引数を CGWindowListOption.optionAll に指定します。

AppDelegate.swift
let windowInfos = CGWindowListCopyWindowInfo([.optionAll], 0)

実際には表示されないウインドウの情報もすべて取得されてしまうため、以下の条件のウインドウを省くことにします。

  • アルファが0
  • ウインドウの矩形サイズが100以下
  • 取得したイメージサイズが1以下
  • アプリ名がDock
  • アプリ名がWindow Server
Window.swift
class Window {
    ...
    init?(with windowInfo: NSDictionary) {
        let windowAlpha = windowInfo[Window.convert(CFString: kCGWindowAlpha)]
        let alpha = windowAlpha != nil ? (windowAlpha as! NSNumber).intValue : 0
        let windowBounds = windowInfo[Window.convert(CFString: kCGWindowBounds)]
        let bounds = windowBounds != nil ? CGRect(dictionaryRepresentation: windowBounds as! CFDictionary) ?? .zero : .zero
        let ownerName = windowInfo[Window.convert(CFString: kCGWindowOwnerName)]
        let name = ownerName != nil ? Window.convert(CFString: ownerName as! CFString) : ""
        let windowId = windowInfo[Window.convert(CFString: kCGWindowNumber)]
        let id = windowId != nil ? Window.convert(CFNumber: windowId as! CFNumber) : 0
        let image = NSImage.windowImage(with: id)

        guard
            alpha > 0,
            bounds.width > 100,
            bounds.height > 100,
            image.size.width > 1,
            image.size.height > 1,
            name != "Dock",
            name != "Window Server" else {
            return nil
        }

        self.id = id
        self.name = name
        self.image = image
    }

ウインドウのスクリーンショット

kCGWindowNumber キーで取得した CGWindowID を元にウインドウのキャプチャイメージを取得します。

Window.swift
let windowId = windowInfo[Window.convert(CFString: kCGWindowNumber)]
let id = windowId != nil ? Window.convert(CFNumber: windowId as! CFNumber) : 0

キャプチャ部分は CGWindowListCreateImage(_:_:_:_:) を使用して CGImage を取得します。

Window.swift
private extension NSImage {
    class func windowImage(with windowId: CGWindowID) -> NSImage {
        if let screenShot = CGWindowListCreateImage(CGRect.null, .optionIncludingWindow, CGWindowID(windowId), CGWindowImageOption()) {
            let bitmapRep = NSBitmapImageRep(cgImage: screenShot)
            let image = NSImage()
            image.addRepresentation(bitmapRep)
            return image
        } else {
            return NSImage()
        }
    }
}