跳转到内容

元素选择器

元素选择器让脚本可以说清楚“我要找哪个界面元素”。它的写法接近 CSS:从元素类型开始,再按标题、状态或层级逐步缩小范围。

const buttons = root.queryElements('Button[title="Save"]');

大多数情况下,可以先在元素菜单模式选中目标,在 复制 子菜单中选择 复制选择器。把结果粘贴进脚本后,再根据实际使用情况稍作简化。

Cursor Crane 生成的选择器会尽量准确地指向当前元素,但界面中的标题、列表内容和网页文本常常会变化。复制后建议先运行一次,再删去不稳定的部分,只留下真正有区分度的条件。

例如,下面的选择器很精确:

Window[title="Project Alpha"] Group Button[title="Run"]

如果项目名经常变化,而对应分组中的 Run 按钮已经足够明确,可以改成:

Group Button[title="Run"]
目标写法示例
按元素类型找ButtonTextFieldListButton
按标题找[title="..."]Button[title="Save"]
按标识符找#idButton#save-button
不写 Kind,按辅助功能 role 找[role="AX..."][role="AXWebArea"]
按状态找:enabled:focusedTextField:focused
找任意元素**[visible=true]

内置支持的 Kind 有:ApplicationUnknownPopoverGroupButtonTextFieldCellImageToolbarScrollAreaOverflowedWebContentListLinkDraggable。Kind 是 Cursor Crane 的内部分类,并不一定对应单个辅助功能 role。在选择器的类型位置,写出的 token 会匹配元素的 Kind 或辅助功能 role;辅助功能 role 可带或不带 AX 前缀,因此 Button 能匹配 AXButton

并非每个辅助功能元素都有这些 Kind。你可以把任意原始辅助功能 role 写在类型位置,例如 WebAreaAXWebArea;也可以完全不写类型,仅用 role 属性筛选:

WebArea
AXWebArea
[role="AXWebArea"]

若要显式写通配符,请用 *,例如 *[role="AXWebArea"]。不确定 Kind 或 role 时,从复制结果开始最可靠。

空格表示“在里面任意位置”,> 表示“直接在里面”,逗号表示“匹配其中任意一个”:

Window Button
Window > Button
Button[title="Save"], Button[title="Cancel"]
  • Window Button 会寻找窗口里任意层级的按钮。
  • Window > Button 只寻找窗口的直接子按钮。
  • 最后一行同时寻找保存和取消按钮。

已经拿到元素时,可以调用 matchesSelector(selector) 验证它是否符合完整选择器,而不必再查询一次。父级和祖先条件也会参与匹配:

if (saveButton?.matchesSelector('Window > Toolbar Button[title="Save"]')) {
await saveButton.performPress();
}

这个方法需要 accessibilityAPI 权限。选择器来自用户输入时请注意:无效选择器会抛出错误。

属性条件支持完全匹配、开头匹配、结尾匹配和包含:

写法含义示例
=完全相同Button[title="Save"]
^=以文字开头StaticText[value^="Error"]
$=以文字结尾TextField[title$="name"]
*=包含文字Button[label*="Continue"]
属性也可写作匹配内容
ididentifier#id 简写辅助功能标识符;若没有,则匹配 DOM 标识符。
domIddomIdentifierDOM 标识符。
role原始辅助功能 role,例如 AXButton
subrole原始辅助功能 subrole。
title元素标题。
labeldescription辅助功能标签或描述。
value元素值。
type元素类型。
enabled元素是否可用。
visible元素是否可见。
hidden元素是否隐藏。
focused元素是否获得焦点。
domClassDOM class token,见下文。

文字里有空格时,请用单引号或双引号包起来。

Button[enabled=true]
TextField[focused=true]
StaticText[value*="Completed"]

domId(也可写作 domIdentifier)和 domClass 是特殊的 DOM 属性,通常只会在浏览器、WebView 或 Electron 应用暴露的内容中可用;原生 macOS 控件一般不会提供它们。仅当复制出的选择器显示这些属性时再使用。

*[domId="compose-button"]
Button[domIdentifier="compose-button"]

domClass 用来提供 DOM class 列表。请将它写成显式属性;不支持 CSS 风格的 class 简写。

Button[domClass="primary active"]
Button[domClass*="primary active"]
  • = 要求 DOM class token 的集合完全相同,顺序无关。
  • *= 要求元素包含列出的每一个 DOM class token。
  • Button.primaryButton[class="primary"] 都是无效写法;请改用 Button[domClass="primary"]

匹配前会按空白拆分 DOM class,因此 menu 不会匹配 menu-item。当选中的元素提供这些属性时,复制出的选择器会带上 domIddomClass

写法含义
:first取第一个匹配结果。
:nth(n)取第 n 个匹配结果,从 1 开始。
:enabled / :disabled只保留可用或不可用的元素。
:visible / :hidden只保留可见或隐藏的元素。
:focused只保留当前获得焦点的元素。
List Row:nth(2) Button:first

这会找到列表中的第二行,再取得那一行里的第一个按钮。

元素查询的效率并不高:queryElements() 会遍历 children,元素层级很大时可能造成明显阻塞。请避免重复执行同一个查询。若多个目标元素有共同父节点,建议先查询一次父节点,再从父节点查询各个目标,而不是每次都从整个窗口开始搜索。对于长列表、表格和滚动区域,queryVisibleElement() 更适合“只看用户此刻看得到的内容”:

const visibleButtons = root.queryVisibleElement('Button:enabled', 50);
const toolbar = root.queryElements('Toolbar[title="Formatting"]', 1)[0];
const boldButton = toolbar?.queryElements('Button[title="Bold"]', 1)[0];
const italicButton = toolbar?.queryElements('Button[title="Italic"]', 1)[0];

这样脚本就不会把注意力放在滚动到很远位置的项目上。两个查询方法都可以传入第二个参数,限制最多返回多少个结果。

先找到窗口,再从窗口开始查询。内置 example.element-query.js 已经演示了完整流程:

const application = ctx.elementInspector.getElementByPid(window.application.pid);
const root = application?.getWindows().find(
element => element.windowId === window.windowId
);
const matches = root?.queryVisibleElement('Button[enabled=true]', 50) ?? [];

查询元素需要在 meta.permissions 中加入 "accessibilityAPI"。找到元素后,你可以高亮它、移动到它,或触发它支持的操作。详见脚本 API