You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在macOS中通过Swift实现全局自定义光标?

如何在macOS中通过Swift实现全局自定义光标?

我完全懂你遇到的痛点——NSCursor确实是AppKit专为单应用设计的,想让所有软件都生效,得跳出单应用的限制,用Core Graphics层面的API来实现。下面我给你一步步拆解可行的方案,都是实际项目里验证过的:

核心思路

全局光标控制得靠Core Graphics的CGDisplaySetCursorImage API,它能直接修改系统级别的光标显示,但有个硬性前提:你的App必须获得辅助功能权限,这是macOS的安全限制,不然所有操作都会静默失败。


第一步:申请辅助功能权限

这是最容易踩坑的环节,必须先搞定:

  1. 在你的App的Info.plist里添加NSAccessibilityUsageDescription,描述清楚为什么需要这个权限(比如“需要权限来全局自定义光标显示”),不然用户在系统设置里看不到权限申请提示。
  2. 代码里先检查权限,没有的话引导用户去系统设置开启:
    import ApplicationServices
    
    func checkAccessibilityPermission() -> Bool {
        let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
        return AXIsProcessTrustedWithOptions(options)
    }
    
    调用这个方法时,如果权限没开,系统会自动弹出引导,提示用户去「系统设置 > 隐私与安全性 > 辅助功能」开启你的App权限。

第二步:准备自定义光标资源

macOS的光标通常支持32x32或64x64的尺寸,Retina屏建议用@2x的图(比如64x64的图对应32pt的光标),并且要带alpha通道(这样透明部分能正常显示,不会出现黑块)。

代码里把UIImage转成CGImage:

func loadCursorCGImage() -> CGImage? {
    guard let image = UIImage(named: "CustomCursor") else { return nil }
    // 确保图片是合适的尺寸,这里以32pt为例
    let targetSize = CGSize(width: 32, height: 32)
    UIGraphicsBeginImageContextWithOptions(targetSize, false, 0.0)
    image.draw(in: CGRect(origin: .zero, size: targetSize))
    let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return scaledImage?.cgImage
}

第三步:设置全局自定义光标

拿到CGImage后,就可以调用Core Graphics的API设置全局光标了。如果有多个显示器,需要遍历所有活跃显示器来设置:

import CoreGraphics

func setGlobalCustomCursor() {
    guard checkAccessibilityPermission() else {
        print("请先开启辅助功能权限")
        return
    }
    
    guard let cursorImage = loadCursorCGImage() else {
        print("加载光标图片失败")
        return
    }
    
    // 获取所有活跃的显示器ID
    var displayCount: UInt32 = 0
    CGGetActiveDisplayList(0, nil, &displayCount)
    let displayIDs = UnsafeMutablePointer<CGDirectDisplayID>.allocate(capacity: Int(displayCount))
    defer { displayIDs.deallocate() }
    CGGetActiveDisplayList(displayCount, displayIDs, &displayCount)
    
    // 给每个显示器设置光标,第二个参数是光标热点(点击的有效位置,比如(16,16)就是中心)
    for i in 0..<Int(displayCount) {
        let displayID = displayIDs[i]
        CGDisplaySetCursorImage(displayID, cursorImage, CGPoint(x: 16, y: 16))
    }
}

第四步:恢复默认光标

一定要记得在App退出、或者你想恢复系统光标时调用这个方法,不然用户的光标会一直停留在自定义状态,体验极差:

func restoreDefaultGlobalCursor() {
    var displayCount: UInt32 = 0
    CGGetActiveDisplayList(0, nil, &displayCount)
    let displayIDs = UnsafeMutablePointer<CGDirectDisplayID>.allocate(capacity: Int(displayCount))
    defer { displayIDs.deallocate() }
    CGGetActiveDisplayList(displayCount, displayIDs, &displayCount)
    
    for i in 0..<Int(displayCount) {
        let displayID = displayIDs[i]
        CGDisplayRestoreCursorImage(displayID)
    }
}

注意事项

  • 权限是硬要求:哪怕你代码写得再对,没开辅助功能权限,CGDisplaySetCursorImage也不会有任何效果,一定要引导用户开启。
  • 光标热点要设对:热点是光标实际触发点击的位置,比如你的光标是十字形,热点应该在中心;如果是箭头,热点在箭头尖,不然用户点击会有明显的错位感。
  • 适配Retina屏:用UIGraphicsBeginImageContextWithOptions时,scale设为0.0会自动适配当前屏幕的scale,避免光标显示模糊。
  • 系统兼容性:这个API从macOS 10.10开始就支持了,现在的主流系统版本都没问题,但如果要兼容更老的版本,可能要做适配检查。

如果你还要做更复杂的全局光标逻辑(比如根据鼠标位置切换、或者响应特定事件),那还得结合全局事件监听(同样需要辅助权限),但基础的全局光标替换用上面的方法就完全够用啦!

火山引擎 最新活动