突出显示鼠标光标下的 NSWindow

deh*_*len 5 macos cocoa appkit swift


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


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

这是该功能应该是什么样子的屏幕截图: 它应该是什么样子

然而,我目前的实现看起来像这样: 它看起来像什么

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

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

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 { $0["kCGWindowLayer"] as! Int == 0 }
            .map { Window(
                frame: CGRect(x: ($0["kCGWindowBounds"] as! [String: Any])["X"] as! CGFloat,
                       y: ($0["kCGWindowBounds"] as! [String: Any])["Y"] as! CGFloat,
                       width: ($0["kCGWindowBounds"] as! [String: Any])["Width"] as! CGFloat,
                       height: ($0["kCGWindowBounds"] as! [String: Any])["Height"] as! CGFloat),
                applicationName: $0["kCGWindowOwnerName"] as! String)}
    }
Run Code Online (Sandbox Code Playgroud)

2.获取鼠标位置

private func registerMouseEvents() {
        NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
            self.mouseLocation = NSEvent.mouseLocation
            return $0
        }
        NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { _ in
            self.mouseLocation = NSEvent.mouseLocation
        }
    }
Run Code Online (Sandbox Code Playgroud)

3. 高亮当前鼠标位置的窗口:

static func window(at point: CGPoint) -> Window? {
        // TODO: only if frontmost
        let list = all()
        return list.filter { $0.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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Sam*_*-IH 4

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

为什么 NSWindow.level 不起作用:

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

使用 NSWindow.order(_:relativeTo) 引入的问题

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

另外,如果您尝试突出显示级别为 的窗口0,您将遇到问题。要解决此类问题,您需要找到要突出显示的窗口的窗口级别,并将突出显示窗口设置为该级别。(我的代码没有包含此问题的修复)

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

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

太长了;向我们展示一些代码

这就是我所做的。

1.更改窗口结构以包含窗口编号:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

2.在您的窗口列表函数 ie 中static func all() -> [Window],包含窗口编号:

refNumber: $0["kCGWindowNumber"] as! Int
Run Code Online (Sandbox Code Playgroud)

3.在窗口突出显示功能中,在 后highlightWindowController?.showWindow(nil),相对于您突出显示的窗口对窗口进行排序!

highlightWindowController!.window!.order(.above, relativeTo: window.windowNumber)
Run Code Online (Sandbox Code Playgroud)

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

window.level = .normal

5.窗口现在将闪烁,为了防止这种情况,请更新视图控制器 if 语句:

    if let window = WindowList.window(at: mouseLocation) {
        if !window.isCapture {
            highlight(window: window)
        }
    } else {
        removeHighlight()
    }
Run Code Online (Sandbox Code Playgroud)

祝你好运,享受快速驾驶的乐趣!

编辑:

我忘了提及,我的 swift 版本是 4.2(尚未升级),因此语法可能略有不同。