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

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/156085/2f728539-333a-2a3b-5959-2d3988e1c3f0.png)


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

* Xcode 9.4.1
* Swift 4.1

# 概要
他のアプリの `NSWindow` や `CGWindowRef` などのウインドウオブジェクトを取得することはできませんが、 `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` クラスを定義します。

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

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

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

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

```Swift:AppDelegate.swift
let windowInfoList = CGWindowListCopyWindowInfo([.optionAll], 0)
```

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

* アルファが0
* ウインドウの矩形サイズが100以下
* 取得したイメージサイズが1以下
* アプリ名がDock
* アプリ名がWindow Server

```Swift: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` を元にウインドウのキャプチャイメージを取得します。

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

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

```Swift: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()
        }
    }
}
```