当前位置: 首页 > news >正文

SwiftUI实现macOS光标高亮工具:原理、开发与优化指南

1. 项目概述:为什么我们需要一个“高亮光标”工具?

如果你经常做屏幕录制、线上会议演示,或者像我一样,有时需要向同事远程讲解一个复杂的软件操作流程,那你一定遇到过这个尴尬时刻:观众在屏幕那头问——“你的鼠标在哪?刚才点哪里了?”。尤其是在高分辨率屏幕上,或者当你的桌面背景颜色和鼠标指针颜色相近时,那个小小的箭头或圆点,很容易就“消失”在屏幕的海洋里。传统的解决方案可能是调大鼠标指针尺寸,或者换成对比度更高的颜色,但这些系统自带的功能往往效果有限,且不够灵活。

这就是focus-cursor这类工具诞生的背景。它不是一个复杂的系统工具,而是一个精准解决单一痛点的小而美应用:实时、醒目地高亮你的鼠标光标。它的核心价值在于,通过一个可自定义颜色、大小和样式的视觉焦点,将你所有的鼠标移动轨迹和点击动作,清晰地“广播”给屏幕前的每一位观众。无论是教学、演示还是日常录屏,它都能极大提升沟通效率和观看体验。

我最初接触这类需求是在做技术分享的时候,后来发现它在很多场景下都很有用。比如产品经理给设计团队演示交互逻辑,或者客服人员远程指导用户操作,一个清晰可见的光标能避免大量无效的来回确认。focus-cursor这个项目,正是用 SwiftUI 为 macOS 平台实现这一功能的轻量级方案。接下来,我会结合自己的使用和开发经验,为你深入拆解这个工具的设计思路、实现细节以及那些官方文档里不会写的实操技巧。

2. 核心功能与设计思路拆解

2.1 功能定位:从“看得见”到“看得清”

focus-cursor的核心功能非常聚焦,就是光标高亮。但一个好的工具,会在“聚焦”的基础上做深度优化。我们来看看它具体解决了哪些问题:

  1. 基础高亮:在光标周围绘制一个醒目的圆形光环或十字准星,这是最基本的功能。但关键在于,这个高亮效果必须是实时、低延迟的。任何肉眼可察的延迟都会导致演示者说的和观众看到的不同步,体验会大打折扣。
  2. 自定义视觉样式:不同场景需要不同的高亮样式。例如,在深色背景的代码编辑器里演示,可能需要一个亮黄色的光环;而在一个浅色的PPT页面里,一个深蓝色的十字线可能更合适。因此,提供颜色、大小、甚至形状(圆形、方形、十字)的自定义选项,是提升工具普适性的关键。
  3. 点击反馈强化:单纯的移动轨迹高亮还不够。当用户点击鼠标(左键、右键)时,需要有一个更强烈的视觉反馈,比如光环瞬间放大并闪烁一下,或者颜色短暂变化。这能明确告知观众“此处发生了交互事件”。
  4. 低干扰与常驻运行:作为一个辅助工具,它必须足够“安静”。它应该常驻在菜单栏或后台,通过一个简单的快捷键(如Cmd+Shift+F)快速开启或关闭高亮,而不影响用户的其他操作。它的界面也应该极其简洁,几乎不需要学习成本。

2.2 技术选型:为什么是 SwiftUI 和 macOS?

从项目关键词(SwiftUI, macOS)可以看出,这是一个原生 macOS 应用。这个选择背后有非常实际的考量:

  • 性能与原生体验:光标高亮需要实时捕获全局的鼠标事件(位置、点击),并立即在屏幕上绘制图形。使用原生框架(如 AppKit 或 SwiftUI)可以直接调用底层的 Quartz 显示服务,实现像素级、低延迟的屏幕绘制,这是 Web 技术或跨平台框架难以媲美的。
  • SwiftUI 的声明式 UI:对于这样一个 UI 相对简单的工具,SwiftUI 是绝佳选择。它的声明式语法让界面开发变得快速直观,状态管理(如当前高亮颜色、是否启用)可以轻松绑定到 UI 控件上。开发者可以更专注于核心的业务逻辑(鼠标追踪与绘制),而非复杂的界面代码。
  • macOS 的系统集成:作为 macOS 专属应用,它可以更好地与系统集成。例如,它可以方便地实现菜单栏常驻(NSStatusItem),支持全局快捷键(MASShortcut等第三方库或NSEvent.addGlobalMonitor),以及遵循 macOS 的人机界面指南,让用户感觉这就是系统的一部分。
  • 轻量与高效:相比于功能庞杂的录屏或演示软件,一个专精于光标高亮的独立 App 更加轻量。它启动快、内存占用小(通常只有几十MB),可以随时待命,不会成为系统的负担。

这个设计思路体现了现代工具软件的一个趋势:用最合适的技术栈,解决一个最明确的问题,并且把体验做到极致focus-cursor没有试图去集成录屏、标注等复杂功能,它只做好“高亮光标”这一件事,反而让它成为了一个不可替代的“螺丝刀”型工具。

3. 核心实现原理与关键技术点

3.1 全局鼠标事件监听

这是整个应用的基石。在 macOS 上,要实时获取鼠标位置和点击事件,通常有几种方式:

  1. NSEvent.addGlobalMonitorForEvents(matching:handler:):这是最常用且相对安全的方法。它可以监听全局的鼠标移动(.mouseMoved)、左键按下/抬起(.leftMouseDown/Up)、右键事件等。监听移动事件时,需要注意性能,因为鼠标移动事件频率极高,处理器函数必须非常高效。

    // 示例代码:监听全局鼠标移动 NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { event in let currentLocation = event.locationInWindow // 注意,这个坐标需要转换到屏幕坐标系 // 更新内部存储的光标位置 self.currentCursorPosition = convertToScreenCoordinates(currentLocation, from: event.window) // 触发界面重绘 self.updateHighlightLayer() }

    注意:使用全局监听需要用户在系统偏好设置 -> 安全性与隐私 -> 辅助功能中,授权该应用控制电脑的权限。这是focus-cursor首次运行时必须引导用户完成的关键步骤,否则监听会失效。

  2. CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:):这是一个更底层的 Core Graphics 事件点击(Event Tap)API。它功能更强大,甚至可以修改或阻止事件。但对于单纯的光标位置读取来说,有点“杀鸡用牛刀”,且更容易触发系统的安全警报,审核上架 Mac App Store 可能更麻烦。focus-cursor这类追求简洁和易用性的工具,通常首选第一种方案。

3.2 屏幕覆盖层绘制

获取到光标位置后,下一步就是在屏幕上绘制高亮图形。这里不能直接在现有的应用窗口上画,因为光标会移动到任何地方,包括其他应用之上。因此,需要创建一个覆盖全屏幕的透明窗口。

  1. 创建透明、无边框、置顶窗口

    let overlayWindow = NSWindow( contentRect: NSScreen.main?.frame ?? .zero, styleMask: [.borderless], backing: .buffered, defer: false ) overlayWindow.backgroundColor = .clear // 关键:窗口完全透明 overlayWindow.isOpaque = false overlayWindow.level = .screenSaver // 或 .statusBar, 确保窗口在最顶层 overlayWindow.ignoresMouseEvents = true // 关键:窗口不拦截任何鼠标点击,鼠标事件能穿透到下层应用 overlayWindow.collectionBehavior = [.stationary, .canJoinAllSpaces, .fullScreenAuxiliary]

    这个窗口将作为我们绘制高亮效果的画布。ignoresMouseEvents = true至关重要,它保证了你的所有点击仍然能正常作用于底层的 Photoshop、浏览器或任何其他应用。

  2. 使用CALayer或 SwiftUICanvas进行绘制:在窗口的内容视图中,我们需要根据光标位置实时绘制一个图形。

    • CALayer方案:更传统,性能极佳。可以创建一个自定义的NSView,在其layer上添加一个CAShapeLayer(用于画圆环)或CATextLayer(用于画十字)。每次鼠标移动时,更新这个ShapeLayerposition
    • SwiftUICanvas方案:更现代,与 SwiftUI 状态绑定结合得更好。在Canvas的闭包中,根据@State存储的光标位置,使用GraphicsContext直接绘制路径。SwiftUI 会自动处理视图更新。
    // SwiftUI Canvas 绘制示例(简化) struct OverlayView: View { @StateObject var cursorTracker = CursorTracker() // 负责监听鼠标的类 var body: some View { Canvas { context, size in let circlePath = Path(ellipseIn: CGRect( x: cursorTracker.position.x - 20, y: cursorTracker.position.y - 20, width: 40, height: 40 )) context.stroke(circlePath, with: .color(.blue), lineWidth: 3) // 如果刚刚发生了点击,再绘制一个内圈闪烁效果 if cursorTracker.didClickRecently { let innerPath = Path(ellipseIn: CGRect(...)) context.fill(innerPath, with: .color(.yellow.opacity(0.7))) } } .ignoresSafeArea() // 覆盖整个屏幕 } }

    绘制逻辑的核心是性能。图形要尽可能简单(避免复杂阴影和模糊效果),重绘区域要精确(只更新光标周围区域,而不是整个屏幕),这样才能保证在60Hz甚至更高刷新率的屏幕上流畅运行。

3.3 状态管理与用户配置

一个友好的工具必须允许用户自定义。focus-cursor需要管理以下状态,并持久化保存:

  • 启用/禁用开关:一个布尔值。
  • 高亮颜色:存储为NSColorCGColor
  • 光环大小:一个CGFloat值。
  • 点击反馈效果开关

这些配置通常使用UserDefaults来存储就足够了。在 SwiftUI 中,可以结合@AppStorage属性包装器,轻松地将界面控件(如ColorPickerSlider)与持久化存储绑定起来。

struct SettingsView: View { @AppStorage("highlightColor") private var colorData: Data? @AppStorage("ringSize") private var ringSize: Double = 40.0 private var highlightColor: Color { // 从 Data 解码出 Color } var body: some View { Form { ColorPicker("高亮颜色", selection: Binding( get: { highlightColor }, set: { newColor in /* 编码并存入 UserDefaults */ } )) Slider(value: $ringSize, in: 20...100, label: { Text("光环大小: \(Int(ringSize))") }) } } }

4. 从零开始的详细实现步骤

假设我们现在要从零开始,用 SwiftUI 实现一个focus-cursor的核心功能。我会把过程拆解得足够细,即使你是 SwiftUI 新手,也能跟着思路走下来。

4.1 项目初始化与权限配置

  1. 创建新的 macOS App 项目:在 Xcode 中,选择 “macOS” -> “App” 模板,语言选择 Swift,界面选择 SwiftUI。
  2. 配置应用权限
    • 在项目导航器中,点击你的项目根目录,选择 “Signing & Capabilities” 标签页。
    • 点击 “+ Capability”,添加 “Hardened Runtime”。(虽然不是必须,但这是上架和增强安全性的好习惯)。
    • 更重要的是,我们需要在Info.plist文件中声明辅助功能权限。右键Info.plist-> “Open As” -> “Source Code”,添加以下键值:
      <key>NSAppleEventsUsageDescription</key> <string>本应用需要监听全局鼠标事件以实现光标高亮功能。</string>
      实际上,对于监听全局鼠标事件,更准确的描述是辅助功能。但上述描述是用户能理解的。更严谨的做法是,应用在首次尝试监听事件时,如果失败,会引导用户去系统设置中开启权限。我们可以在代码中实现这个引导。

4.2 构建核心鼠标追踪器

创建一个名为CursorTracker.swift的类,它负责与系统交互,获取光标位置。

import SwiftUI import Cocoa class CursorTracker: ObservableObject { @Published var position: CGPoint = .zero @Published var isClicking: Bool = false private var eventMonitor: Any? func startMonitoring() { // 检查辅助功能权限(简化检查,实际需更严谨) let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary let isTrusted = AXIsProcessTrustedWithOptions(options) if !isTrusted { print("请前往 系统设置 > 隐私与安全性 > 辅助功能, 并授权本应用。") // 这里可以触发一个Alert,提示用户去开启权限 return } // 监听全局鼠标移动 eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { [weak self] event in DispatchQueue.main.async { // 将窗口坐标转换为屏幕坐标 if let screen = NSScreen.main { let screenPoint = NSPointToCGPoint(event.locationInWindow) // 注意:event.locationInWindow 在全局监听中可能不准确, // 更可靠的方式是使用 CGEvent 或 NSEvent.mouseLocation self?.position = NSEvent.mouseLocation // 直接使用这个更简单 // NSEvent.mouseLocation 的Y坐标原点在屏幕左下角,而SwiftUI原点在左上角,需要转换 self?.position.y = screen.frame.maxY - self!.position.y } } } // 监听鼠标按下事件 // 注意:.leftMouseDown 是局部事件,全局监听需要用 .leftMouseDownMask (在mask中) // 更简单的方式:使用一个本地事件监听器来检测点击,结合全局位置 let localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .leftMouseUp]) { [weak self] event in self?.isClicking = (event.type == .leftMouseDown) return event // 如果不希望修改事件,就原样返回 } // 需要保存这个 localMonitor 以便后续移除 } func stopMonitoring() { if let monitor = eventMonitor { NSEvent.removeMonitor(monitor) eventMonitor = nil } } deinit { stopMonitoring() } }

实操心得NSEvent.mouseLocation是获取光标当前位置最直接的方法,它返回的是屏幕坐标系下的点,其中Y坐标原点在屏幕左下角。而 SwiftUI 的坐标系原点在左上角。这个坐标转换是初期最容易踩的坑之一,如果不转换,你绘制的高亮会上下颠倒。转换公式很简单:screenHeight - mouseLocation.y

4.3 创建覆盖全屏的透明窗口

我们不修改主窗口,而是创建一个新的、专门用于绘制的窗口。在App文件中操作。

import SwiftUI @main struct FocusCursorApp: App { @StateObject private var cursorTracker = CursorTracker() @State private var overlayWindow: NSWindow? var body: some Scene { WindowGroup { ContentView(cursorTracker: cursorTracker) .onAppear { setupOverlayWindow() cursorTracker.startMonitoring() } .onDisappear { cursorTracker.stopMonitoring() } } .windowStyle(.hiddenTitleBar) // 主窗口也可以简化样式 .commands { // 可以在这里添加快捷键命令 } } private func setupOverlayWindow() { // 确保只在主线程操作UI DispatchQueue.main.async { if overlayWindow == nil { let screenFrame = NSScreen.main?.frame ?? .zero let window = NSWindow( contentRect: screenFrame, styleMask: [.borderless], backing: .buffered, defer: false ) window.backgroundColor = .clear window.isOpaque = false window.level = .screenSaver // 确保在最前 window.ignoresMouseEvents = true // 允许鼠标穿透 window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary] window.isReleasedWhenClosed = false let overlayView = NSHostingView(rootView: OverlayView(cursorTracker: cursorTracker)) overlayView.autoresizingMask = [.width, .height] window.contentView = overlayView window.makeKeyAndOrderFront(nil) overlayWindow = window } } } }

4.4 实现高亮绘制视图

现在实现OverlayView,这是显示高亮效果的核心。

struct OverlayView: View { @ObservedObject var cursorTracker: CursorTracker @AppStorage("highlightColor") private var colorData: Data = Color.blue.encodeToData() // 需要扩展Color @AppStorage("ringRadius") private var ringRadius: Double = 25.0 @AppStorage("showClickEffect") private var showClickEffect: Bool = true // 计算属性:从存储的Data解码出Color private var highlightColor: Color { Color.decode(from: colorData) ?? .blue } var body: some View { Canvas { context, size in // 1. 绘制外圈光环 let outerCircle = Path(ellipseIn: CGRect( x: cursorTracker.position.x - ringRadius, y: cursorTracker.position.y - ringRadius, width: ringRadius * 2, height: ringRadius * 2 )) var outerStrokeStyle = StrokeStyle(lineWidth: 3.0, lineCap: .round, lineJoin: .round) context.stroke(outerCircle, with: .color(highlightColor), style: outerStrokeStyle) // 2. 如果正在点击,绘制一个内圈填充作为反馈 if cursorTracker.isClicking && showClickEffect { let innerRadius = ringRadius * 0.5 let innerCircle = Path(ellipseIn: CGRect( x: cursorTracker.position.x - innerRadius, y: cursorTracker.position.y - innerRadius, width: innerRadius * 2, height: innerRadius * 2 )) // 使用半透明的白色填充,制造“闪烁”感 context.fill(innerCircle, with: .color(.white.opacity(0.6))) } // 3. (可选) 绘制一个十字准星,更精确地指示光标尖端 let crossHairLength: CGFloat = 15.0 let horizontalLine = Path { p in p.move(to: CGPoint(x: cursorTracker.position.x - crossHairLength, y: cursorTracker.position.y)) p.addLine(to: CGPoint(x: cursorTracker.position.x + crossHairLength, y: cursorTracker.position.y)) } let verticalLine = Path { p in p.move(to: CGPoint(x: cursorTracker.position.x, y: cursorTracker.position.y - crossHairLength)) p.addLine(to: CGPoint(x: cursorTracker.position.x, y: cursorTracker.position.y + crossHairLength)) } context.stroke(horizontalLine, with: .color(highlightColor.opacity(0.8)), style: StrokeStyle(lineWidth: 1.5)) context.stroke(verticalLine, with: .color(highlightColor.opacity(0.8)), style: StrokeStyle(lineWidth: 1.5)) } .ignoresSafeArea() // 覆盖整个屏幕,包括菜单栏和Dock区域 .allowsHitTesting(false) // 确保视图本身不拦截任何鼠标事件 } } // 简单的 Color 编码/解码扩展(实际项目可能需要更健壮的方案) extension Color { func encodeToData() -> Data { let nsColor = NSColor(self) return try! NSKeyedArchiver.archivedData(withRootObject: nsColor, requiringSecureCoding: false) } static func decode(from data: Data) -> Color? { guard let nsColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: data) else { return nil } return Color(nsColor) } }

4.5 构建用户控制界面

主窗口ContentView应该非常简洁,只提供必要的控制项。

struct ContentView: View { @ObservedObject var cursorTracker: CursorTracker @State private var isHighlightEnabled: Bool = true var body: some View { VStack(spacing: 20) { Toggle("启用光标高亮", isOn: $isHighlightEnabled) .onChange(of: isHighlightEnabled) { newValue in if newValue { cursorTracker.startMonitoring() } else { cursorTracker.stopMonitoring() } } .toggleStyle(.switch) .controlSize(.large) Divider() Text("高亮设置") .font(.headline) ColorPicker("选择高亮颜色", selection: Binding( get: { // 这里需要从 @AppStorage 读取,为了简化示例,我们假设有一个共享的 ViewModel // 实际项目中,颜色状态应放在一个统一的 SettingsViewModel 中 return .blue }, set: { newColor in // 保存到 UserDefaults } )) .padding(.horizontal) Slider(value: .constant(25.0), in: 15...60) { Text("光环大小") } minimumValueLabel: { Image(systemName: "circle") } maximumValueLabel: { Image(systemName: "circle.inset.filled") } .padding(.horizontal) Toggle("点击反馈效果", isOn: .constant(true)) Spacer() Text("提示:首次使用请确保在系统设置中授予辅助功能权限。") .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding() } .padding() .frame(width: 300, height: 400) } }

5. 打包、分发与进阶优化思路

5.1 应用打包与签名

开发完成后,你需要将应用打包分发给其他人使用。

  1. 配置发布信息:在 Xcode 项目设置中,确保设置了正确的Bundle Identifier(如com.yourname.focus-cursor)、版本号和构建版本。
  2. 代码签名:对于非 App Store 分发,你可以使用 “Developer ID Application” 证书进行签名。这能保证应用在非开发机器上打开时,不会立即被系统拦截(尽管用户可能仍需在“安全性与隐私”中手动允许)。
  3. 打包归档:在 Xcode 菜单栏选择Product->Archive。归档成功后,在 Organizer 窗口中,点击Distribute App。如果只是给少数人用,选择 “Copy App”,它会生成一个.app文件。你可以直接压缩这个.app文件为.zip进行分发。
  4. 公证(Notarization):为了让你的应用在 macOS Catalina 及更高版本上顺利运行(避免“无法打开,因为无法验证开发者”的警告),强烈建议进行公证。这需要通过 Xcode 的 “Distribute App” 流程,选择 “Developer ID” 并上传到 Apple 进行公证。公证成功后,应用会获得一个“门票”(ticket),用户打开时体验会好很多。

5.2 进阶功能与优化建议

一个基础版本完成后,可以考虑以下方向进行增强,这也是区分优秀工具和普通工具的关键:

  1. 性能优化

    • 按需绘制:目前的Canvas会在每次鼠标移动时重绘整个屏幕区域。虽然 SwiftUI 做了优化,但更高效的方式是使用CALayer,并只更新图层的位置和属性,而不是重绘整个路径。
    • 降低监听频率:对于鼠标移动事件,如果不需要极其平滑的轨迹(比如只用于演示),可以设置一个阈值,只有当光标移动超过一定像素距离时才更新位置,或者使用一个低通滤波器来平滑移动并减少更新次数。
  2. 功能增强

    • 多屏幕支持:现在的实现假设只有一个主屏幕。需要修改为遍历NSScreen.screens,为每个屏幕创建一个覆盖窗口,并将光标坐标正确转换到对应的屏幕空间。
    • 高亮样式库:除了颜色,可以提供预设的样式,如“呼吸灯效果”、“雷达扫描效果”、“点击涟漪扩散效果”等。
    • 快捷键全局切换:集成HotKey库(如Sauce),实现按Cmd+Shift+F快速开启/关闭高亮,而无需切回应用窗口。
    • 菜单栏常驻:将主窗口关闭,改为一个菜单栏应用。点击菜单栏图标可以弹出设置面板。这更符合这类辅助工具的定位(focus-cursor的 Releases 中提供的.app文件很可能就是这种形式)。
  3. 稳定性与用户体验

    • 完善的权限引导:在应用启动时,如果检测到没有辅助功能权限,应弹出一个友好的、带图示的引导窗口,直接告诉用户点击后会自动跳转到系统设置的对应页面。
    • 开机自启:提供选项,让应用在登录时自动启动,并最小化到菜单栏。
    • 内存与电池优化:确保应用在后台时(高亮关闭状态)几乎不消耗 CPU 和内存资源。

6. 常见问题排查与实战心得

在实际开发和使用过程中,你肯定会遇到一些坑。以下是我总结的一些典型问题及其解决方案:

6.1 权限问题导致监听失效

  • 现象:应用启动后,光标高亮完全不工作,控制台没有报错。
  • 排查
    1. 首先检查应用是否出现在系统设置 > 隐私与安全性 > 辅助功能的列表中,并且开关已被打开。
    2. 如果没有,需要在代码中主动请求权限。可以使用AXIsProcessTrusted()函数检查,如果返回false,则弹窗提示用户,并用NSWorkspace.shared.open(URL(string: \"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility\")!)直接打开系统设置对应页面。
    3. 即使权限已开启,有时系统可能需要重启应用才能生效。
  • 心得权限处理是 macOS 桌面工具开发的第一道坎。一定要把用户体验做好,用最清晰的方式引导用户,一次成功。否则用户很可能因为“用不起来”而直接放弃。

6.2 高亮图形闪烁或卡顿

  • 现象:光标移动时,高亮光环出现明显的闪烁、拖影或延迟。
  • 排查与解决
    1. 绘制性能:检查CanvasCALayer的绘制代码是否过于复杂。避免在每一帧都创建新的Path对象,可以复用。对于静态的图形样式,使用displayList预编译。
    2. 事件频率:鼠标移动事件NSEvent.mouseMoved频率极高。在事件处理函数中不要做任何耗时操作(如文件读写、网络请求)。只做最简单的坐标更新和界面标记。
    3. 线程问题:确保所有 UI 更新都在主线程 (DispatchQueue.main.async) 中进行。NSEvent的回调可能不在主线程。
    4. Vsync 同步:确保你的绘制与屏幕刷新率同步。SwiftUI 的Canvas在这方面处理得较好。如果使用CALayer,可以考虑使用CADisplayLink来驱动动画更新。

6.3 在多显示器环境下位置错乱

  • 现象:有两个显示器,高亮只在一个屏幕上显示,或者位置完全不对。
  • 解决
    1. 坐标系转换:牢记NSEvent.mouseLocation返回的是全局屏幕坐标系,原点在主屏幕的左下角。而每个NSScreen有自己的frame(原点也在全局坐标系中)。你需要判断当前光标位于哪个屏幕的frame内。
    2. 为每个屏幕创建窗口:遍历NSScreen.screens,为每个屏幕创建一个全屏的透明覆盖窗口。当鼠标移动时,计算它在哪个屏幕内,然后只更新(或显示)那个屏幕对应的覆盖窗口上的高亮图层。
    3. 坐标归一化:在绘制时,需要将全局坐标转换为当前窗口所在屏幕的局部坐标。公式类似于:localX = globalX - screen.frame.minX,localY = globalY - screen.frame.minY(注意Y轴方向)。

6.4 应用无法常驻后台或菜单栏

  • 现象:关闭主窗口后,应用就退出了。
  • 解决:你需要将应用改为Agent Application菜单栏应用
    1. Info.plist中,将Application is agent (UIElement)设置为YES。这样应用就不会在 Dock 显示图标,也不会显示菜单栏。
    2. 在应用启动时 (AppDelegateAppinit),创建NSStatusItem(菜单栏图标)。
    3. 将主要的设置界面放在一个Window中,并通过点击状态栏图标来弹出/收回这个窗口。主WindowGroup可以设置为隐藏。
  • 心得:对于这种“工具型”应用,菜单栏是绝佳的归宿。它不占用 Dock 位置,不干扰用户工作流,随时可取用,非常符合“随手工具”的定位。

6.5 打包后在其他电脑上无法打开

  • 现象:在自己电脑上运行正常,打包发给别人后,提示“已损坏,无法打开”。
  • 解决:这通常是 macOS 的 Gatekeeper 安全机制导致的。
    1. 首选方案:进行Developer ID 公证,如上文所述。这是最正规的途径。
    2. 临时测试方案:让用户在终端执行命令sudo xattr -rd com.apple.quarantine /Applications/YourApp.app来移除隔离属性。或者,让他们在“安全性与隐私”设置中,点击“仍要打开”。(注意:这需要告知用户操作步骤,体验不好)。
    3. 避免使用:网上有些教程说在打包时用codesign --force --deep --sign -命令重签名,这可能会带来其他问题,不推荐作为最终方案。

开发像focus-cursor这样一个小而精的工具,整个过程就像在打磨一件顺手的手工器具。最大的成就感来自于它切实地解决了一个高频、细微但真实的痛点。从技术上看,它涉及了 macOS 的事件系统、图形绘制、多窗口管理、权限模型和用户体验设计等多个层面,是一个非常好的综合性练手项目。希望这篇超详细的拆解,不仅能帮你理解和使用这个工具,更能给你带来自己动手创造类似工具的灵感和信心。

http://www.jsqmd.com/news/748956/

相关文章:

  • 告别模糊屏和断网!用NootedRed+AX210在小新Pro16上打造完美黑苹果工作站的实战记录
  • 2026全国音乐喷泉生产厂家标杆名录及地址一览:酒店喷泉/音乐喷泉制作/音乐喷泉安装设计/音乐喷泉设计公司/音乐喷泉设计安装/选择指南 - 优质品牌商家
  • 基于MCP与多源数据构建AI人才情报分析系统
  • 2026年4月保利中心做得好的秀禾服租赁品牌口碑推荐,新娘妆造/订婚礼服租赁/主持人礼服租赁,秀禾服租赁机构哪家靠谱 - 品牌推荐师
  • 体验 Taotoken 多模型聚合路由带来的高稳定性与低延迟
  • 项目实训个人博客记录(四)——医院智能辅助诊疗与院内资源调度平台:基于 Vue 3 + Vite 的三端平台原型改造与实现
  • 新手避坑指南:用Colab T4 GPU复现STGCN交通预测模型(附完整环境配置)
  • 效率提升:快马生成jdk17全平台自动化安装与校验脚本
  • 告别迷茫!用SSCTOOL和Excel表格,手把手搞定你的第一个EtherCAT从站代码
  • 命令行数据分析利器:analytics-cli 流式处理与插件化架构实战
  • 2026威克防霉片技术解析:蓝色防霉片、迈可达防霉片、防潮干燥剂、霉克星防霉片、食品干燥剂、香包干燥剂、香型干燥剂选择指南 - 优质品牌商家
  • Arm Cortex-A53 SystemC Cycle模型解析与应用
  • Agent 火到离谱,但真正让它跑起来的不是热搜,而是向量引擎这种 API 中转底座
  • 告别重复编码:用快马平台结合aigc,自动化生成前端项目骨架
  • 深度学习分布式训练:负载均衡与通信优化实战
  • 独立开发者如何借助 Taotoken 以更低成本试用主流大模型
  • PedGPT:基于YOLOv8与GPT-4的行人检测与自然语言描述系统实践
  • 观察不同时段调用 Taotoken 服务的稳定性与路由容错表现
  • 云原生会话审计:非侵入式追踪与OpenTelemetry集成实践
  • solidworks新手福音:用快马ai生成互动学习工具,轻松掌握基础操作
  • AI辅助开发:为寻亲动画注入智能对话与剧情续写能力
  • ai辅助开发:让快马平台智能生成wsl ubuntu配置方案,自适应不同开发者需求
  • RepoMemory:为AI编程助手构建本地记忆层,解决会话无状态痛点
  • MicroPython v1.27版本更新解析与嵌入式开发实践
  • 2.4 采购部门——权力来自信息不对称
  • Go语言构建高性能WebSocket服务器:从Hub模型到生产级实时协作引擎
  • 从零打造一个“跳一跳”:在HarmonyOS模拟器上用Canvas复刻经典
  • 到底什么是智能体?一篇文章带你真正搞明白
  • 神经网络优化器:从原理到实战,提升模型性能的关键秘籍
  • 给数学老师的Python礼物:用Manim从零制作你的第一个教学动画(附完整代码)