デスクトップ上にあるウインドウの一覧を取得する


はじめに

Mac ネタでなんか書こうと色々考えてたのですが、久々に Cocoa を触っていたら思った以上に忘れてしまっていたために戸惑ってしまいました。

その中でウインドウをハックするネタを考えていましたが、まずは全ウインドウ一覧的なものを取得するための方法を記事にしようと思います。

macOS のウインドウは WindowServer が管理している

WindowServer というヤツは kernel_task と並んでたまに CPU リソースを消費している変なヤツぐらいの認識ですが、調べると CoreGraphcis の一部に組み込まれていて、実際に macOS のウインドウを管理しているプロセスだということがわかります。その歴史は古く、NEXTSTEP の時代から今に至っているようです。

ちなみに似たようなヤツで、ステータスメニュー/メニューエクストラ(メニューバー右側領域)などを管理する SystemUIServer なんてのもいますね。

参考:OS X ハッキング! 189 古より伝わる「WindowServer」をイジる

この WindowServer にアクセスすれば、デスクトップ上にあるウインドウの一覧を取得することができそうです。

10.5以降ではウインドウリストへのアクセスが簡単にできる

10.4以前までは Carbon をいじったりPrivate APIを使ったりと本当に難しかったのですが、10.5からは Cocoa で簡単にウインドウリストを取得することができるようになりました。
と言っても今は10.12の時代なので一体いつから時間が止まってるんだという話でもありますが……。

@available(OSX 10.5, *)
public func CGWindowListCopyWindowInfo(_ option: CGWindowListOption, _ relativeToWindow: CGWindowID) -> CFArray?

Swift で書くとこんな感じですかね。Swift で CFArray とかつらみしかないので、NSArray に一旦キャストしてから Swift 配列に変換します。中身はとりあえず NSDictionary として扱います。

func getWindowList() -> [NSDictionary]? {
    guard let windowList: NSArray = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) else {
        return nil
    }

    let swiftWindowList = windowList as! [NSDictionary]

    return swiftWindowList
}

CGWindowListCopyWindowInfo() の第一引数にはどんなウインドウの情報が欲しいかを指定します。CGWindowListOption という OptionSet があるので、この中から適当なものを選びます。

CGWindowListOption
// ユーザーセッションにおける、オンスクリーン、オフスクリーン含む全てのウインドウのリスト。第2引数には `kCGNullWindowID` を指定しなければならない。
public static var optionAll: CGWindowListOption { get }

// ユーザーセッションにおける、全てのオンスクリーンウインドウのリスト。並び順はウインドウの順番である。第2引数には `kCGNullWindowID` を指定しなければならない。
public static var optionOnScreenOnly: CGWindowListOption { get }

// 第2引数に指定した Window ID より上にある全てのオンスクリーンウインドウのリスト。並び順はウインドウの順番である。
public static var optionOnScreenAboveWindow: CGWindowListOption { get }

// 第2引数に指定した Window ID より下にある全てのオンスクリーンウインドウのリスト。並び順はウインドウの順番である。
public static var optionOnScreenBelowWindow: CGWindowListOption { get }

// 第2引数に指定した Window ID のウインドウをリストに含める
public static var optionIncludingWindow: CGWindowListOption { get }

// デスクトップの要素をリストから除外する
public static var excludeDesktopElements: CGWindowListOption { get }

第2引数には Window ID (Window Number) を指定します。特になければ kCGNullWindowID が使用可能です。

実行結果
めちゃくちゃ大量の [NSDictionary] が出力された

Safari とか Xcode とかあるので、どうやら全てのウインドウの一覧っぽいリストが取得できました。Touch Bar 搭載 Mac だと Touch Bar 上のウインドウっぽいものも含まれているようです。

ウインドウリストから必要な情報を取り出す

.optionAll を指定してしたので、オフスクリーンと思われるウインドウも含まれてしまいました。その辺は前述したオプションを駆使すれば除外もできますが、filter で適当に除外してみます。

func getWindowList() -> [NSDictionary]? {
    guard let windowList: NSArray = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) else {
        return nil
    }

    let swiftWindowList = windowList as! [NSDictionary]

    let myWindowList = swiftWindowList.filter { (windowInfo: NSDictionary) -> Bool in
        // kCGWindowIsOnscreen (CFBoolean) が含まれていてかつ true かどうかでオンスクリーンを判定
        if let isOnscreen = windowInfo[kCGWindowIsOnscreen] as? Bool, isOnscreen == true {
            // アプリのウインドウのみを抽出
            let bundleName = Bundle.main.infoDictionary![kCFBundleNameKey as String] as! String
            return windowInfo[kCGWindowOwnerName] as! String == bundleName
        }

        return false
    }

    return myWindowList
}
実行結果
アプリのオンスクリーンウインドウのみ出力された

このような感じで、割と簡単にデスクトップ上にあるウインドウの一覧を取得することができました。

まとめ

macOS の WindowServer が管理するウインドウの一覧を Swift のコードで取得する方法を説明しました。

CoreGraphics の API を駆使すれば簡単にアプリケーションをまたいでウインドウの情報を取得することができます。CGWindowListCopyWindowInfo() で適切なオプションを指定すると辞書の配列が返ってくるので、そこから Window ID (Window Number) やデスクトップ上におけるウインドウのフレーム値などを取り出して、ウインドウのハックに活かすことができそうです。