You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

macOS用Accessibility API获取焦点元素光标位置在Electron应用失效

问题根因

Electron类应用(含Slack)光标坐标获取错误,核心是遗漏了无障碍返回值的坐标系校验与转换逻辑,不需要针对Electron调用特殊私有API适配,补全坐标系转换逻辑即可覆盖所有同类应用场景。
macOS原生App的kAXBoundsForRangeParameterizedAttribute返回的CGRect默认使用全局屏幕坐标系:原点在屏幕左下角,Y轴向上。但Electron内嵌的Chromium无障碍实现,返回的范围矩形是相对于当前焦点元素所属顶层AX窗口的局部坐标系:原点在窗口内容区左上角,Y轴向下,你拿到的X=0、Y=屏幕高度的值,就是未做转换的局部坐标直接被当成全局坐标解析的结果。


遗漏的必加处理逻辑
  • 每次调用AXUIElementCopyParameterizedAttributeValue必须校验返回的AXError状态,非success状态直接返回nil,避免拿到未初始化的脏值。
  • 拿到范围矩形后,必须先递归查找当前焦点UI元素所属的顶层AX窗口对象,再获取该窗口的全局位置与尺寸。
  • 增加坐标系类型判断:如果返回的矩形范围落在窗口局部尺寸区间内,判定为局部坐标,做两层转换:
    1. 局部X值叠加窗口全局X坐标,得到全局X位置
    2. 做Y轴方向转换:全局Y位置 = 窗口全局Y原点(屏幕左下角为基准) + 窗口高度 - 局部Y值 - 光标矩形高度
  • 多显示器场景下不要硬编码主屏幕高度计算坐标,通过窗口对象读取的全局位置可自动适配多屏偏移。
  • 调用位置接口前校验元素的kAXFocusedAttribute是否为true,避免拿到元素失焦后的过期坐标值。

修复后的参考实现
extension AXUIElement {
    // 递归获取元素所属的顶层窗口
    private func hostWindow() -> AXUIElement? {
        var role: CFTypeRef?
        guard AXUIElementCopyAttributeValue(self, kAXRoleAttribute as CFString, &role) == .success,
              let roleStr = role as? String else {
            var parent: CFTypeRef?
            guard AXUIElementCopyAttributeValue(self, kAXParentAttribute as CFString, &parent) == .success else {
                return nil
            }
            return (parent as! AXUIElement).hostWindow()
        }
        if roleStr == kAXWindowRole as String {
            return self
        }
        var parent: CFTypeRef?
        guard AXUIElementCopyAttributeValue(self, kAXParentAttribute as CFString, &parent) == .success else {
            return nil
        }
        return (parent as! AXUIElement).hostWindow()
    }

    func getCursorRect() -> CGRect? {
        guard let cursorPosition = cursorPosition else { return nil }
        var cfRange = CFRange(location: cursorPosition, length: 1)
        guard let axval = AXValueCreate(.cfRange, &cfRange) else { return nil }
        
        var bounds: CFTypeRef?
        let axError = AXUIElementCopyParameterizedAttributeValue(
            self,
            kAXBoundsForRangeParameterizedAttribute as CFString,
            axval,
            &bounds
        )
        guard axError == .success, let bounds = bounds else { return nil }
        
        var cursorRect = CGRect.zero
        guard AXValueGetValue(bounds as! AXValue, .cgRect, &cursorRect) else { return nil }
        
        // 拿到所属窗口的全局位置和尺寸
        guard let window = hostWindow() else { return cursorRect }
        var windowPos = CGPoint.zero
        var windowSize = CGSize.zero
        var posValue: CFTypeRef?
        var sizeValue: CFTypeRef?
        
        guard AXUIElementCopyAttributeValue(window, kAXPositionAttribute as CFString, &posValue) == .success,
              AXValueGetValue(posValue as! AXValue, .cgPoint, &windowPos),
              AXUIElementCopyAttributeValue(window, kAXSizeAttribute as CFString, &sizeValue) == .success,
              AXValueGetValue(sizeValue as! AXValue, .cgSize, &windowSize) else {
            return cursorRect
        }
        let windowRect = CGRect(origin: windowPos, size: windowSize)

        // 判定是否为局部坐标:矩形范围落在窗口尺寸内则做转换,否则视为原生全局坐标直接返回
        let isLocalRect = cursorRect.origin.x >= -10
                        && cursorRect.origin.y >= -10
                        && cursorRect.maxX <= windowRect.size.width + 10
                        && cursorRect.maxY <= windowRect.size.height + 10
        guard isLocalRect else {
            return cursorRect
        }

        // 局部坐标转全局屏幕坐标
        cursorRect.origin.x += windowRect.origin.x
        // 左上角原点Y轴向下 转 左下角原点Y轴向上
        cursorRect.origin.y = windowRect.origin.y + windowRect.size.height - cursorRect.origin.y - cursorRect.size.height
        
        return cursorRect
    }
}

内容的提问来源于stack exchange,提问作者Arul Murugan Sivapoosam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 17:48:17