突出显示鼠标光标下的 NSWindow

Highlight NSWindow under mouse cursor


由于这是相当多的代码,如果有一个示例项目可以帮助您更好地理解当前的问题,我制作了一个简单的示例项目,您可以在 GitHub 此处找到它:https://github.com/dehlen/Whosebug


我想实现一些与 macOS 屏幕截图工具非常相似的功能。当鼠标悬停在 window 上时,应该突出显示 window。但是,我遇到的问题只是突出显示了用户可见的 window 部分。

这是该功能的屏幕截图:

我目前的实现是这样的:

我当前的实现执行以下操作:

1.获取屏幕上所有 windows 可见的列表

static func all() -> [Window] {
        let options = CGWindowListOption(arrayLiteral: .excludeDesktopElements, .optionOnScreenOnly)
        let windowsListInfo = CGWindowListCopyWindowInfo(options, CGMainDisplayID()) //current window
        let infoList = windowsListInfo as! [[String: Any]]
        return infoList
            .filter { [=10=]["kCGWindowLayer"] as! Int == 0 }
            .map { Window(
                frame: CGRect(x: ([=10=]["kCGWindowBounds"] as! [String: Any])["X"] as! CGFloat,
                       y: ([=10=]["kCGWindowBounds"] as! [String: Any])["Y"] as! CGFloat,
                       width: ([=10=]["kCGWindowBounds"] as! [String: Any])["Width"] as! CGFloat,
                       height: ([=10=]["kCGWindowBounds"] as! [String: Any])["Height"] as! CGFloat),
                applicationName: [=10=]["kCGWindowOwnerName"] as! String)}
    }

2。获取鼠标位置

private func registerMouseEvents() {
        NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
            self.mouseLocation = NSEvent.mouseLocation
            return [=11=]
        }
        NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { _ in
            self.mouseLocation = NSEvent.mouseLocation
        }
    }

3。高亮当前鼠标位置的window:

static func window(at point: CGPoint) -> Window? {
        // TODO: only if frontmost
        let list = all()
        return list.filter { [=12=].frame.contains(point) }.first
    }
var mouseLocation: NSPoint = NSEvent.mouseLocation {
        didSet {
            //TODO: don't highlight if its the same window
            if let window = WindowList.window(at: mouseLocation), !window.isCapture {
                highlight(window: window)
            } else {
                removeHighlight()
            }
        }
    }

 private func removeHighlight() {
        highlightWindowController?.close()
        highlightWindowController = nil
    }

    func highlight(window: Window) {
        removeHighlight()
        highlightWindowController = HighlightWindowController()
        highlightWindowController?.highlight(frame: window.frame, animate: false)
        highlightWindowController?.showWindow(nil)
    }

class HighlightWindowController: NSWindowController, NSWindowDelegate {
    // MARK: - Initializers
    init() {
        let bounds = NSRect(x: 0, y: 0, width: 100, height: 100)
        let window = NSWindow(contentRect: bounds, styleMask: .borderless, backing: .buffered, defer: true)
        window.isOpaque = false
        window.level = .screenSaver
        window.backgroundColor = NSColor.blue
        window.alphaValue = 0.2
        window.ignoresMouseEvents = true
        super.init(window: window)
        window.delegate = self
    }

    // MARK: - Public API
    func highlight(frame: CGRect, animate: Bool) {
        if animate {
            NSAnimationContext.current.duration = 0.1
        }
        let target = animate ? window?.animator() : window
        target?.setFrame(frame, display: false)
    }
}

如您所见,光标下方的 window 已突出显示,但突出显示 window 绘制在可能相交的其他 windows 上方。

可能的解决方案 我可以遍历列表中可用的 windows,只找到不与其他 windows 重叠的矩形,只为这部分而不是整个 window 绘制高亮矩形。

我在问自己这是否是解决此问题的更优雅、更高效的解决方案。也许我可以用绘制的 HighlightWindow 的 window 级别来解决这个问题?或者我可以利用 Apple 的任何 API 来获得所需的行为吗?

我不习惯 Swift,抱歉,但在我看来,自然的解决方案是使用 - orderWindow:relativeTo:。在 ObjC 中(在显示高亮 window 之后添加):

[highlightWindow orderWindow:NSWindowAbove relativeTo:window];

并让 window 服务器处理隐藏隐藏部分的所有细节。当然,当用户在 on-screen 周围移动东西时,将突出显示 window 保持在目标 window 的正上方会产生不同的麻烦,但是...

我弄乱了你的代码,@Ted 是正确的。 NSWindow.order(_:relativeTo) 正是您所需要的。

为什么 NSWindow.level 不起作用:

使用 NSWindow.level 对您不起作用,因为普通 windows(如您屏幕截图中的那些)都具有 window 级别 0,或 .normal。如果您只是将 window 级别调整为例如“1”,您的突出显示视图将出现在 所有 和另一个 windows 之上。相反,如果您将它设置为“-1”,您的突出显示视图将显示在 下方 所有正常 windows 和桌面上方。

使用NSWindow.order(_: relativeTo)

引入的问题

没有很好的解决方案就没有警告吗?为了使用此方法,您必须将 window 级别设置为 0,以便它可以在其他 windows 之间分层。但是,这将导致在 WindowList.window(at: mouseLocation) 方法中选择突出显示 window。当它被选中时,您的 if-statement 将其删除,因为它认为它是主要的 window。这会导致闪烁。 (下面的 TLDR 中包含对此的修复)

此外,如果您尝试突出 window 而不是 的水平 0,您将 运行 成为问题.要解决此类问题,您需要找到要突出显示的 window 的 window 级别,并将突出显示 window 设置为该级别。 (我的代码没有包含对这个问题的修复)

除了上述问题之外,您还需要考虑当用户将鼠标悬停在背景上 window 并在不移动鼠标的情况下单击它时会发生什么。会发生什么是背景 window 将变成前面.. 没有 移动突出显示 window。一个可能的解决方法是更新点击事件的高亮 window。

最后,我注意到您每次用户移动鼠标时都会创建一个新的 HighlightWindowController + window。如果您只是在鼠标移动时改变已经存在的 HighlightWindowController 的框架(而不是创建一个),系统可能会稍微轻一些。要隐藏它,您可以调用 NSWindowController.close() 函数,甚至将框架设置为 {0,0,0,0}(不确定第二个想法)。

TLDR;给我们一些代码

这是我所做的。

1. 更改您的 window 结构以包含一个 window 数字:

struct Window {
    let frame: CGRect
    let applicationName: String
    let windowNumber: Int

    init(frame: CGRect, applicationName: String, refNumber: Int) {
        self.frame = frame.flippedScreenBounds
        self.applicationName = applicationName
        self.windowNumber = refNumber
    }

    var isCapture: Bool {
        return applicationName.caseInsensitiveCompare("Capture") == .orderedSame
    }
}

2. 在您的 window 列表功能中,即 static func all() -> [Window],包括 window 号码:

refNumber: [=11=]["kCGWindowNumber"] as! Int

3. 在你的 window 高亮函数中,在 highlightWindowController?.showWindow(nil) 之后,相对于你的 window 命令 window正在突出显示!

highlightWindowController!.window!.order(.above, relativeTo: window.windowNumber)

4. 在高亮控制器中,确保将 window 级别设置回正常:

window.level = .normal

5. window 现在会闪烁,为防止这种情况,请更新您的视图控制器 if-statement:

    if let window = WindowList.window(at: mouseLocation) {
        if !window.isCapture {
            highlight(window: window)
        }
    } else {
        removeHighlight()
    }

祝你好运,玩得开心 swifting!

编辑:

我忘记说了,我的 swift 版本是 4.2(还没有升级)所以语法可能略有不同。