如何在macOS中通过Swift实现全局自定义光标?
如何在macOS中通过Swift实现全局自定义光标?
我完全懂你遇到的痛点——NSCursor确实是AppKit专为单应用设计的,想让所有软件都生效,得跳出单应用的限制,用Core Graphics层面的API来实现。下面我给你一步步拆解可行的方案,都是实际项目里验证过的:
核心思路
全局光标控制得靠Core Graphics的CGDisplaySetCursorImage API,它能直接修改系统级别的光标显示,但有个硬性前提:你的App必须获得辅助功能权限,这是macOS的安全限制,不然所有操作都会静默失败。
第一步:申请辅助功能权限
这是最容易踩坑的环节,必须先搞定:
- 在你的App的
Info.plist里添加NSAccessibilityUsageDescription,描述清楚为什么需要这个权限(比如“需要权限来全局自定义光标显示”),不然用户在系统设置里看不到权限申请提示。 - 代码里先检查权限,没有的话引导用户去系统设置开启:
调用这个方法时,如果权限没开,系统会自动弹出引导,提示用户去「系统设置 > 隐私与安全性 > 辅助功能」开启你的App权限。import ApplicationServices func checkAccessibilityPermission() -> Bool { let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true] return AXIsProcessTrustedWithOptions(options) }
第二步:准备自定义光标资源
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开始就支持了,现在的主流系统版本都没问题,但如果要兼容更老的版本,可能要做适配检查。
如果你还要做更复杂的全局光标逻辑(比如根据鼠标位置切换、或者响应特定事件),那还得结合全局事件监听(同样需要辅助权限),但基础的全局光标替换用上面的方法就完全够用啦!




