中文亮度
本地双语对照页 · 非官方 · 译文:claude-opus-5[1m] · effort high · 260727 · 原文

Core Graphics Event Services

Core Graphics 事件服务 · 四份材料合读
Apple Developer Documentation & Apple Developer Forums
文档页无标注日期 · 论坛帖 Feb '23 · 取于 260727
为什么合读这四份 · 我们要解一个具体问题:能不能在不抢焦点、不动光标的前提下,操作后台应用的界面?实测结果是「键盘可以、鼠标不行」。这四份材料合起来正好解释了为什么——第一份画出事件流水线,第二份交代 event tap 的设计初衷,第三份给出三个截获点的准确位置,第四份是另一个使用者撞上同一堵墙的记录。

Ⅰ. CGEvent · Overview / 事件流水线

Defines an opaque type that represents a low-level hardware event.

定义一个不透明类型,代表一次底层硬件事件。

Low-level hardware events of this type are referred to as Quartz events.

这类底层硬件事件称作 Quartz 事件。

A typical event in macOS originates when the user manipulates an input device such as a mouse or a keyboard.

macOS 中一个典型的事件,起于用户操作输入设备——如鼠标或键盘。

The device driver associated with that device, through the I/O Kit, creates a low-level event, puts it in the window server's event queue, and notifies the window server.

该设备对应的驱动程序,经由 I/O Kit 造出一个底层事件,把它放进窗口服务器(window server)的事件队列,并通知窗口服务器。

The window server creates a Quartz event, annotates the event, and dispatches the event to the appropriate run-loop port of the target process.

窗口服务器据此造出一个 Quartz 事件,为它做标注(annotate),再把它派发到目标进程相应的 run-loop 端口。

There the event is picked up by the Carbon Event Manager and forwarded to the event-handling mechanism appropriate to the application environment.

事件在那里被 Carbon Event Manager 取走,转交给适合该应用环境的事件处理机制。

You can use event taps to gain access to Quartz events at several different steps in this process.

你可以用 event tap(事件截获点)在这条流程的若干不同环节上取得 Quartz 事件。

This opaque type is derived from CFType and inherits the properties that all Core Foundation types have in common.

这个不透明类型派生自 CFType,继承了所有 Core Foundation 类型共有的性质。

全篇枢纽在第五句 · annotate(标注)这一步,决定了鼠标事件「该给哪个窗口、哪个视图」。

CGEventPost(kCGHIDEventTap, …) 把事件塞进链路开头,窗口服务器照常标注、照常派发 → 鼠标有效。
CGEventPostToPid(pid, …) 直接投到目标进程的 run-loop 端口跳过了标注 → 鼠标缺了归属信息被丢弃;而键盘事件不依赖标注(送给该进程的主窗口即可),故仍然有效。

这就是 260727 实测「键盘可、鼠标不可」的机制解释。

Ⅱ. Quartz Event Services · Overview / 截获点的由来

Provides features for managing event taps—filters for observing and altering the stream of low-level user input events in macOS.

提供管理 event tap 的功能——所谓 event tap,是用来观察并改动 macOS 底层用户输入事件流的过滤器。

Event taps make it possible to monitor and filter input events from several points within the system, prior to their delivery to a foreground application.

event tap 让你能在系统内部的若干点上监视并过滤输入事件——在这些事件送达前台应用之前。

Event taps complement and extend the capabilities of the Carbon event monitor mechanism, which allows an application to observe input events delivered to other processes.

event tap 补充并扩展了 Carbon 事件监视机制的能力——后者允许一个应用观察送往其他进程的输入事件。

Event taps are designed to serve as a Section 508 enabling technology.

event tap 的设计初衷,是作为一项符合「第 508 条」的无障碍使能技术。

For example, consider a software system to assist a person with language impairments, designed to perform keyboard filtering with spoken review.

举例来说,设想一套辅助语言障碍者的软件,其设计是做键盘过滤并配以语音复述。

Such a system could use an event tap to monitor all keystrokes, perform dictionary checks and matches, and recite the assembled word back to the user on detection of a word break in the input stream.

这样一套系统可以用 event tap 监视全部按键,做词典查验与匹配,并在输入流中检测到词的边界时,把拼成的词朗读回给用户。

If acceptable to the user, as indicated by an additional input keystroke or other gesture, the events would be posted into the system for delivery to the foreground application.

若用户认可(以再按一个键或其他手势表示),这些事件才会被投入系统,送往前台应用。

Introduced in OS X version 10.4, event taps provide functionality similar to the Win32 functions SetWinEventHook when used to establish an out-of-context event hook, and SendInput.

event tap 引入于 OS X 10.4,其功能类似于 Win32 的 SetWinEventHook(用于建立进程外事件钩子时)与 SendInput。

Quartz Event Services also includes an older set of event-related functions declared in the file CGRemoteOperation.h.

Quartz Event Services 里还包含一套更老的事件相关函数,声明在 CGRemoteOperation.h 文件中。

These functions are still supported, but they are not recommended for new development.

这些函数仍受支持,但不建议在新开发中使用。

两条值得记的背景 · ① Section 508 是美国《康复法案》第 508 条,要求联邦机构采购的电子信息技术必须对残障人士可用。这套「能截获、能改动、能重新投递用户输入」的能力,本是为无障碍造的——今天被自动化工具借用,属于顺带。
② 最后一句解释了一个常见困惑:网上老代码里能看到另一套事件函数名(出自 CGRemoteOperation.h),那不是错的,是旧的——仍能用,但新写代码别选它。

Ⅲ. CGEventTapLocation / 三个截获点

Constants that specify possible tapping points for events.

一组常量,指定事件可能的截获点。

kCGHIDEventTap — Specifies that an event tap is placed at the point where HID system events enter the window server.

kCGHIDEventTap —— 截获点设在 HID 系统事件进入窗口服务器的那一点。

kCGSessionEventTap — Specifies that an event tap is placed at the point where HID system and remote control events enter a login session.

kCGSessionEventTap —— 截获点设在 HID 系统事件与远程控制事件进入某个登录会话的那一点。

kCGAnnotatedSessionEventTap — Specifies that an event tap is placed at the point where session events have been annotated to flow to an application.

kCGAnnotatedSessionEventTap —— 截获点设在会话事件已被标注、即将流向某个应用的那一点。

In addition to the three tapping points described above, an event tap may also be placed where annotated events are delivered to a specific application.

除上述三个截获点外,还可以把 event tap 放在「已标注事件送达特定应用」之处。

HID = Human Interface Device(人机接口设备),即键盘、鼠标、触控板这类输入硬件的统称。

三档全在窗口服务器的链路上——所以经由它们投递的事件,标注照做、命中归属照算,鼠标事件因此有效。这从反面印证了:postToPid 之所以对鼠标失效,正因为它不在这条链路上

Ⅳ. Forums #724835 / 一位使用者撞上同一堵墙

CGEventPostToPid not posting to background app's open dialog
提问者 StewMacLean · Feb '23 · 分类 Graphics & Games / Core Graphics · 至今 0 回复

Previously, I have used CGEventPost to post events to another application (Logic) which works provided it is in focus.

此前我一直用 CGEventPost 向另一个应用(Logic)投递事件,只要它处于焦点状态就能工作。

I'm actually sending events to a dialog that I've opened via OSC, which causes Logic to "bounce" in the dock indicating that the user needs to respond to the dialog.

我实际是在向一个对话框投递事件——那对话框是我经 OSC 打开的,它会让 Logic 在 Dock 里跳动,提示用户需要响应。

As I'm getting some "flashing" due to focus switching, I've tried using CGEventPostToPid to post to the application directly.

因为切换焦点造成画面闪烁,我改用 CGEventPostToPid 直接投给该应用。

The theory is that it no longer needs to be in focus, so will obviate the need for focus switching.

设想是这样它就不再需要处于焦点状态,从而省掉切换焦点这一步。

Unfortunately this doesn't work.

可惜行不通。

I still need to bring Logic into focus for the dialog to respond to the sent events.

我仍须把 Logic 切到前台,那个对话框才会响应所投的事件。

I can see that the main window of Logic has lost focus, which implies that, from its point of view the dialog has focus - I would have thought that it would respond to the background event posts.

我能看到 Logic 的主窗口已失去焦点,这说明在它看来对话框是有焦点的——我本以为它会响应后台投来的事件。

Any pointers on how to get this to work appreciated. Just wondering if I need to send events directly to the dialog window somehow?

若有让它跑通的思路,感激不尽。只是在想,我是不是得设法把事件直接投给那个对话框窗口?

Note that I'm doing this to work around a positioning bug in the OSC interface when songs have mixed time signatures.

附带一提,我做这些是为了绕开 OSC 界面的一个定位缺陷——曲子里混用多种拍号时会出问题。

关键在标题里那个词:dialog(对话框) · 他投的目标是模态对话框,而我们 260727 在计算器上验的是普通窗口。两者不矛盾,边界因此更精细:

普通窗口 + 后台键盘投递 —— 成立(计算器实测)
模态对话框 + 后台投递 —— 不成立(此帖)

模态对话框抢走了应用内 key window 的身份,应用内部另有一套事件路由,合成事件送到进程后仍被挡下。故这条能力随目标窗口的性质而变,逐案实测,勿一概而论。

合读所得

① 事件的正路:输入设备 → I/O Kit 驱动 → 窗口服务器队列 → 造 Quartz 事件并标注 → 派发到目标进程 run-loop 端口 → Carbon Event Manager → 应用。

② 两种投递的分别CGEventPost 走正路(三档截获点全在链路上),CGEventPostToPid 抄近道直投进程。

③ 抄近道的代价:跳过标注 → 鼠标失去窗口归属 → 被丢弃;键盘不需归属 → 仍可送达。

④ 边界:后台键盘投递对普通窗口成立,对模态对话框不成立。

⑤ 元事实postToPid 这个函数苹果没写行为文档(无摘要、无讨论、只有一行签名),以上全靠实测与推理。