如何让NSWindow始终置于指定应用窗口下方且无闪烁?
实现macOS窗口始终置于目标窗口下方且保持可交互
核心思路
你的核心需求是窗口可交互(接收键盘、调整大小)但始终位于指定窗口下方,之前的方案失效是因为标题栏/窗口调整事件由NSWindow直接处理,自定义NSView无法拦截。需要从窗口层级控制和事件拦截两个层面入手,结合系统API和私有API实现。
可行解决方案
1. 自定义NSWindow子类拦截全局窗口事件
通过重写NSWindow的sendEvent、becomeKey、becomeMain方法,拦截所有可能触发窗口前置的操作,并强制维持层级:
class BackgroundStickyWindow: NSWindow { // 持有目标窗口的弱引用,避免循环引用 weak var targetWindow: NSWindow? override func sendEvent(_ event: NSEvent) { // 拦截标题栏点击、窗口调整等可能触发前置的事件 let eventTypes: [NSEvent.EventType] = [.leftMouseDown, .rightMouseDown, .otherMouseDown] if eventTypes.contains(event.type) { let hitArea = hitTest(event.locationInWindow) let shouldPreventOrdering = [ .titlebar, .resizeLeft, .resizeRight, .resizeTop, .resizeBottom, .resizeTopLeft, .resizeTopRight, .resizeBottomLeft, .resizeBottomRight ].contains(hitArea) if shouldPreventOrdering { NSApplication.shared.preventWindowOrdering() } } super.sendEvent(event) // 事件处理后强制将窗口放回目标窗口下方 targetWindow.map { self.order(.below, relativeTo: $0.windowNumber) } } override func becomeKey() { super.becomeKey() // 即使窗口成为key(接收键盘输入),也要维持层级 targetWindow.map { self.order(.below, relativeTo: $0.windowNumber) } } override func becomeMain() { super.becomeMain() targetWindow.map { self.order(.below, relativeTo: $0.windowNumber) } } }
2. 使用私有API减少层级调整闪烁
系统默认的order(.below)方法可能存在延迟,导致短暂闪烁。可以使用NSWindow的私有方法直接设置层级关系(注意:私有API会影响App Store审核,仅用于非上架场景):
// 在需要调整层级的位置替换order方法 if let targetWindow = targetWindow { // 私有API:_setOrderingMode:relativeToWindow:,参数1代表"below" self.perform(Selector(("_setOrderingMode:relativeToWindow:")), with: 1, with: targetWindow) }
3. 确保窗口可交互的关键设置
在初始化窗口时,需要配置以下属性保证交互性:
let window = BackgroundStickyWindow( contentRect: NSRect(x: 0, y: 0, width: 400, height: 300), styleMask: [.titled, .resizable, .closable, .miniaturizable], backing: .buffered, defer: false ) window.acceptsFirstMouse = true window.makeFirstResponder(window.contentView) // 绑定目标窗口,例如获取系统前置应用的主窗口 window.targetWindow = NSWorkspace.shared.frontmostApplication?.windows.first
关键说明
- 标题栏/窗口调整事件由
NSWindow直接处理,必须在sendEvent中拦截,自定义NSView无法覆盖这些区域的事件 - 允许窗口成为
keyWindow才能接收键盘输入,因此不能完全阻止becomeKey,而是在激活后立刻回调层级 - 私有API的使用需要关闭Swift的严格类型检查(在Build Settings中关闭
Swift Compiler - Strict Checking下的Implicitly Unwrapped Optionals)
内容的提问来源于stack exchange,提问作者George C.
相关产品推荐
相关产品推荐

