LoginSignup
39
22

More than 1 year has passed since last update.

Macでウィンドウをいい感じに配置するツール再現してみた。

Last updated at Posted at 2021-08-05

BetterSnapToolMagnetのようなmacOS用のウィンドウ配置マネージャーツールを再現してみました。現在の最前面のウィンドウを右半分に配置したり、左3分の1に配置したり、最大限広げて配置したりといったことがメニューバーのコマンドやショートカットキーをトリガーとして行えます。
screenshot.png

動作の様子
demo.gif

動機

そもそも普通のアプリというのはSandBoxに守られているため、外部のアプリの管轄であるウィンドウを制御することは本来できません。常駐型アプリであること、別アプリのウィンドウを制御していることに興味があったためどのような技術でこれが実現できているのか調査したいと思いました。

技術に関して

アプリを常駐させる

常駐型アプリを作る方法については別途記事があるのでこちらをご参考に。
https://zenn.dev/kyome/articles/02d9f969fd17e5

ウィンドウを制御する

まず、最前面にあるウィンドウのアプリを取得するには、NSWorkspaceの API を用いれば良いです。

let app = NSWorkspace.shared.runningApplications.first(where: { $0.isActive })

あとは、ApplicationServicesを介してアクセシビリティの API を使いウィンドウを制御しました。AXUIElementというオブジェクトでウィンドウの情報を取得したり上書きすることが可能です。上で取得したアプリのpidを用いるとAXUIElementを生成できます。

AXUIElementCopyAttributeValueという API を用いると指定したAUIElementの情報を取得することができます。

// 頻繁に使うため便利関数化
func copyAttributeValue(_ element: AXUIElement, attribute: String) -> CFTypeRef? {
    var ref: CFTypeRef? = nil
    let error = AXUIElementCopyAttributeValue(element, attribute as CFString, &ref)
    if error == .success {
        return ref
    }
    return .none
}

// 情報を取得する例
// ウィンドウの役割をStringで取得
func getRole(element: AXUIElement) -> String? {
    return self.copyAttributeValue(element, attribute: kAXRoleAttribute) as? String
}

// ウィンドウがフルスクリーンかどうかをBoolで取得
func isFullscreen(element: AXUIElement) -> Bool {
    let result = self.copyAttributeValue(element, attribute: "AXFullScreen") as? NSNumber
    return result?.boolValue ?? false
}

attribute にkAXから始まるキーを指定してCFTypeRef = AnyObject型の情報を取得して、のちに適切な型に変換します。ウィンドウの座標や大きさなどの情報をCGPointCGSizeで取得する場合は、さらにAXValueGetValueというAPIを重ねて使用する必要があります。GtiHubでコード検索するとやり方はわかります。

AUIElementの情報を上書きする場合は、AXUIElementSetAttributeValueというAPIを用います。

// 頻繁に使うため便利関数化
func setAttributeValue(_ element: AXUIElement, attribute: String, value: CFTypeRef) -> Bool {
    let error = AXUIElementSetAttributeValue(element, attribute as CFString, value)
    return error == .success
}

// 情報を上書きする例
// 座標を更新
@discardableResult
func setPosition(element: AXUIElement, position: CGPoint) -> Bool {
    var position = position
    if let value = AXValueCreate(AXValueType.cgPoint, &position) {
        return self.setAttributeValue(element, attribute: kAXPositionAttribute, value: value)
    }
    return false
}

更新したい値をAXValueの型に変換してから、kAXから始まるキーと一緒に渡せば更新できます。ウィンドウの制御の根幹はこんな感じです。

制限事項

  • 現在は、アクセシビリティの API をApp SandBox環境では叩けないようになっているため。SandBoxを外す必要があります。必然的に App Store で配信することは不可能です。BetterSnapToolやMagnetは規制が厳しくなかった頃からリリースされていたから特別に許されているという感じでしょうか。ただ、そもそもバイナリ提出で却下される気がするので、特別な App Sandbox Temporary Exception Entitlements を指定しているかもしれません。
  • アクセシビリティの API を叩くには専用の権限をユーザーに付与してもらう必要があります。システム環境設定 -> セキュリティとプライバシー -> アクセシビリティの項目でアプリごとに許可を切り替えられます。故に現在権限が付与されているか確認し、ユーザに催促する仕組みが必要です。

配布

GitHubにソースもアプリも公開してあります。
もっとカスタマイズしたい方のコミットをお待ちしています。
https://github.com/Kyome22/ShiftWindow

39
22
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
39
22