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窗口对象,再获取该窗口的全局位置与尺寸。
- 增加坐标系类型判断:如果返回的矩形范围落在窗口局部尺寸区间内,判定为局部坐标,做两层转换:
- 局部X值叠加窗口全局X坐标,得到全局X位置
- 做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
相关产品推荐
相关产品推荐

