You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在SwiftUI中监听macOS所有应用的窗口开闭事件?

解决方案:监听macOS应用窗口的打开/关闭事件

前提:开启辅助功能权限

你的应用必须在「系统设置 > 隐私与安全性 > 辅助功能」中被授权,否则Accessibility相关API无法正常工作。

核心实现思路

通过AXObserver监听目标应用的kAXWindowsAttribute属性变化,当窗口列表更新时,对比前后的窗口集合来判断是窗口打开还是关闭事件;同时监听应用启动事件,为新启动的常规应用动态注册监听。

完整代码示例

import Cocoa
import Accessibility

class WindowMonitor: NSObject {
    // 存储各应用当前的窗口列表,用于对比变化
    private var appWindows: [pid_t: [AXUIElement]] = [:]
    // 存储已创建的AXObserver实例,避免重复注册
    private var observers: [pid_t: AXObserver] = [:]
    
    override init() {
        super.init()
        // 为已运行的常规应用初始化监听
        registerObserversForRunningApps()
        
        // 监听新应用启动事件,动态注册监听
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleAppLaunch(_:)),
            name: NSWorkspace.didLaunchApplicationNotification,
            object: nil
        )
    }
    
    private func registerObserversForRunningApps() {
        let workspace = NSWorkspace.shared
        let regularApps = workspace.runningApplications.filter { $0.activationPolicy == .regular }
        regularApps.forEach { app in
            guard let pid = app.processIdentifier as? pid_t else { return }
            registerObserver(for: pid)
        }
    }
    
    @objc private func handleAppLaunch(_ notification: Notification) {
        guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
              app.activationPolicy == .regular,
              let pid = app.processIdentifier as? pid_t else { return }
        registerObserver(for: pid)
    }
    
    private func registerObserver(for pid: pid_t) {
        guard observers[pid] == nil else { return }
        
        var observer: AXObserver?
        let callback: AXObserverCallback = { _, element, _, refcon in
            guard let monitor = refcon?.assumingMemoryBound(to: WindowMonitor.self).pointee else { return }
            monitor.handleWindowChange(for: element, pid: pid)
        }
        
        // 创建AXObserver实例
        let createError = AXObserverCreate(pid, callback, &observer)
        guard createError == .success, let observer = observer else {
            print("Failed to create observer for pid \(pid): \(createError.rawValue)")
            return
        }
        
        // 注册窗口变化通知
        let appElement = AXUIElementCreateApplication(pid)
        let registerError = AXObserverAddNotification(
            observer,
            appElement,
            kAXWindowsChangedNotification as CFString,
            Unmanaged.passUnretained(self).toOpaque()
        )
        
        if registerError == .success {
            observers[pid] = observer
            // 将观察者加入RunLoop,确保能接收回调
            CFRunLoopAddSource(
                CFRunLoopGetCurrent(),
                AXObserverGetRunLoopSource(observer),
                CFRunLoopMode.defaultMode
            )
            // 初始化当前窗口列表
            updateCurrentWindows(for: pid)
        } else {
            print("Failed to register windows notification for pid \(pid): \(registerError.rawValue)")
        }
    }
    
    private func updateCurrentWindows(for pid: pid_t) {
        let appElement = AXUIElementCreateApplication(pid)
        var value: AnyObject?
        let error = AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &value)
        guard error == .success, let windows = value as? [AXUIElement] else {
            appWindows[pid] = []
            return
        }
        appWindows[pid] = windows
    }
    
    private func handleWindowChange(for element: AXUIElement, pid: pid_t) {
        let appElement = AXUIElementCreateApplication(pid)
        var value: AnyObject?
        let error = AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &value)
        guard error == .success, let currentWindows = value as? [AXUIElement] else { return }
        
        let previousWindows = appWindows[pid] ?? []
        
        // 处理窗口打开事件
        let newWindows = currentWindows.filter { !previousWindows.contains($0) }
        if !newWindows.isEmpty {
            handleNewWindow(appPID: pid, window: newWindows.first!)
        }
        
        // 处理窗口关闭事件
        let closedWindows = previousWindows.filter { !currentWindows.contains($0) }
        if !closedWindows.isEmpty {
            handleClosedWindow(appPID: pid, window: closedWindows.first!)
        }
        
        // 更新存储的窗口列表
        appWindows[pid] = currentWindows
    }
    
    private func handleNewWindow(appPID: pid_t, window: AXUIElement) {
        // 示例:针对Chrome新窗口执行操作
        let workspace = NSWorkspace.shared
        if let app = workspace.runningApplications.first(where: { $0.processIdentifier == appPID }),
           app.bundleIdentifier == "com.google.Chrome" {
            print("Chrome新窗口已激活,执行自定义操作")
            // 在这里添加你的业务逻辑
        }
    }
    
    private func handleClosedWindow(appPID: pid_t, window: AXUIElement) {
        // 窗口关闭后的自定义逻辑
    }
}

// 使用示例
let monitor = WindowMonitor()
CFRunLoopRun() // 保持RunLoop运行,否则监听会终止

关键注意事项

  • 权限验证:首次运行时系统会弹出权限申请提示,务必在系统设置中完成授权,否则所有Accessibility API会返回权限错误。
  • 应用兼容性:部分应用可能限制Accessibility访问,或窗口结构特殊,需要针对性调整监听逻辑。
  • 内存管理:需妥善维护AXObserver和窗口列表的存储,避免内存泄漏。
  • RunLoop依赖:必须将AXObserver的RunLoopSource加入当前RunLoop,否则无法接收回调事件。

内容的提问来源于stack exchange,提问作者J brian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 00:50:36