如何为macOS 14实现全局应用窗口自定义固定按钮?
实现方案概述
要在所有macOS 14应用的窗口交通按钮(关闭/最小化/最大化)旁添加固定按钮并实现阻止关闭功能,核心依赖辅助功能(Accessibility)框架,同时需要处理窗口监听与视图注入。以下是分步实现细节:
1. 申请辅助功能权限
你的应用必须获取用户的辅助功能权限,才能访问和修改其他应用的窗口元素:
- 在Xcode的
Info.plist中添加NSAccessibilityUsageDescription键,填入权限用途说明(例如:"需要访问其他应用窗口以添加固定按钮")。 - 首次启动时引导用户前往「系统设置>隐私与安全性>辅助功能」,开启你的应用权限。
2. 监听系统窗口创建/销毁事件
实时捕获所有打开的应用窗口,确保新窗口出现时自动添加按钮:
import Cocoa import Accessibility class WindowMonitor { private var observer: AXObserver? init() { let systemWideElement = AXUIElementCreateSystemWide() AXObserverCreate(systemWideElement, windowChangeHandler, &observer) // 监听窗口创建通知 AXObserverAddNotification(observer, systemWideElement, kAXWindowCreatedNotification as CFString, nil) // 将观察者加入运行循环 let runLoop = CFRunLoopGetCurrent() AXObserverAddToRunLoop(observer, runLoop, CFRunLoopMode.defaultMode.rawValue) } private func windowChangeHandler(observer: AXObserver, element: AXUIElement, notification: CFString, userData: UnsafeMutableRawPointer?) { // 给新创建的窗口添加固定按钮 attachPinButton(to: element) } }
3. 给目标窗口插入自定义按钮
要把按钮放到交通按钮旁,需定位窗口的标题栏视图,计算位置后插入按钮:
private func attachPinButton(to windowAXElement: AXUIElement) { // 匹配AXElement对应的NSWindow guard let targetWindow = NSApplication.shared.windows.first(where: { $0.axElement == windowAXElement }) else { return } // 定位系统标题栏视图(不同应用可能有差异,需适配) guard let titleBarView = targetWindow.contentView?.superview?.subviews.first(where: { $0.className == "NSTitleBarView" }) else { return } // 创建固定按钮 let pinButton = NSButton(frame: NSRect(x: 0, y: 0, width: 18, height: 18)) pinButton.image = NSImage(systemSymbolName: "pin.fill", accessibilityDescription: "固定窗口") pinButton.bezelStyle = .circular pinButton.target = self pinButton.action = #selector(toggleWindowPin(_:)) pinButton.tag = targetWindow.hashValue // 标记所属窗口 // 定位交通按钮组,计算按钮插入位置 if let trafficButtons = titleBarView.subviews.first(where: { $0.className == "NSTrafficLightButtons" }) { let pinX = trafficButtons.frame.maxX + 8 // 与交通按钮保持8pt间距 pinButton.frame.origin = CGPoint(x: pinX, y: (titleBarView.frame.height - pinButton.frame.height)/2) titleBarView.addSubview(pinButton) } }
4. 实现固定/取消固定逻辑
方式1:禁用关闭按钮(公开API)
点击按钮时,通过Accessibility API切换目标窗口关闭按钮的启用状态:
@objc private func toggleWindowPin(_ sender: NSButton) { guard let targetWindow = NSApplication.shared.windows.first(where: { $0.hashValue == sender.tag }) else { return } let windowAXElement = targetWindow.axElement // 获取关闭按钮的AXElement var closeButtonAXElement: AXUIElement? AXUIElementCopyAttributeValue(windowAXElement, kAXCloseButtonAttribute as CFString, &closeButtonAXElement) guard let closeButton = closeButtonAXElement else { return } let isPinned = !sender.isEnabled // 更新按钮图标 pinButton.image = NSImage(systemSymbolName: isPinned ? "pin.slash.fill" : "pin.fill", accessibilityDescription: nil) // 切换关闭按钮的可用状态 AXUIElementSetAttributeValue(closeButton, kAXEnabledAttribute as CFString, !(isPinned as CFBoolean)) }
方式2:拦截窗口关闭事件(进阶,需注入)
如果需要彻底阻止窗口关闭(包括快捷键强制关闭),可以通过方法Swizzling替换NSWindow的windowShouldClose:方法,但此操作涉及代码注入,可能违反Mac App Store规则:
extension NSWindow { static func swizzleCloseHandler() { let originalSelector = #selector(windowShouldClose(_:)) let swizzledSelector = #selector(swizzled_windowShouldClose(_:)) guard let originalMethod = class_getInstanceMethod(self, originalSelector), let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) else { return } method_exchangeImplementations(originalMethod, swizzledMethod) } @objc private func swizzled_windowShouldClose(_ sender: Any?) -> Bool { // 判断当前窗口是否被固定,返回false阻止关闭 if isWindowPinned(self) { return false } // 调用原方法逻辑 return self.swizzled_windowShouldClose(sender) } }
兼容性与注意事项
- macOS 14对Accessibility API和窗口层级限制更严格,部分私有视图类名可能变更,需测试适配不同应用。
- 自定义标题栏的应用(如部分第三方软件)可能无法通过上述方法定位交通按钮,需额外适配。
- 应用需保持后台运行,才能持续监听窗口变化并维护按钮状态。
替代方案
若上述实现难度过高,可简化为:
- 做一个全局菜单栏应用,提供「固定/取消固定当前窗口」的快捷功能,无需修改其他应用标题栏。
- 设置全局快捷键,触发时通过Accessibility API切换目标窗口关闭按钮的状态。
内容的提问来源于stack exchange,提问作者Strange-Guy Person
相关产品推荐
相关产品推荐

