NVDA 内幕故事7:API 和覆盖类
本文为《内幕故事》系列英文译文的第七篇,也是关于 NVDA 对象的三部曲的最后一部分。
对于新读者:欢迎来到《NVDA 内幕故事》系列,
一位经验丰富的 NVDA 开发人员将带您揭秘 NVDA 屏幕阅读器的内部工作原理。
由于我们讨论的是软件内部原理,这些帖子很快就会变得非常复杂,所以请耐心阅读。
到目前为止,我们已经讨论了对象名称与角色和 NVDA 对象的解剖。
本文将讨论 API 和覆盖类,并对 NVDA 对象进行一些评论。
我可能会稍微扩展一下,讨论一两个行为类,并且在未来的文章中可以详细介绍进度条输出和建议宣告
(关于各种 NVDA 行为类,如表格导航、对话框、进度条等的讨论将需要一篇完整的文章)。
回顾
NVDA 对象是单个 GUI 控件的抽象(理想化)表示。
NVDA 对象本身不会为您提供有用的信息,也不会提供帮助您与给定控件有效交互的方法。
因此,它依赖于更具体的实现,例如无障碍 API 对象,来提供诸如对象导航、焦点和前台宣告等功能背后的实际机制。
在讨论 NVDA 对象的构造时,我展示了一个“抽象类”的示例 —— NVDA 如何通过各种方式获取对象位置
(展示了三种方式:基本 NVDA 对象、IAccessible 和 UIA),最后我展示了 NVDA 对象的类继承树。
如果您想了解更多关于我刚才讨论的内容,请点击上面的“NVDA 对象解剖”链接
(从某种程度上来说,您需要阅读过 NVDA 对象文章的前两部分,因为我接下来要描述的内容建立在它们之上)。
回想一下,程序员可以用类和对象来表示世界,而继承的定义则使这种表示更加丰富和灵活。
简而言之,继承是指自定义对象或类可以从基类(父类)继承功能,而自定义类可以出于各种原因编辑或替换父类的部分功能。
例如,基类可以定义一个 name
方法,继承自基类的类可以自定义该方法以提供更多详细信息。
有关快速示例,请参阅上一篇《内幕故事》文章中的对象位置示例。
NVDA 对象派生类
如果只提供基本 NVDA 对象 (NVDAObjects.NVDAObject
),NVDA 将缺乏灵活性。
它无法为应用程序提供自定义命令,无法获取一两个自定义控件的标签和角色,
无法自动读取对话框文本,也无法提供浏览模式...
总之,如果只有基本 NVDA 对象,NVDA 将只能提供一种方法。
幸运的是,NVDA 对象是一个类,而 Python 以面向对象编程为范式,允许您自定义获取控件标签和角色的方式。
而这正是 API、行为和覆盖类发挥作用的地方
(我之所以在这里提到行为类,是因为一些插件确实带有从行为派生出来的覆盖类。例如进度条)。
API、行为和覆盖类可以被视为 NVDA 基础对象之上的层。
窗口和行为
第一层是窗口和行为。在 Microsoft Windows 中,控件具有所谓的“窗口句柄”,用于区分各个控件
(您可能会看到文档中用“hwnd
”来指代窗口句柄)。
为了容纳窗口句柄和其他属性,在 NVDA 基础对象之上定义了一个窗口类 (NVDAObjects.window.Window
)。
这是必要的,因为如果没有窗口句柄,NVDA 将会:
- 无法区分 GUI 控件
- 无法创建或与无障碍 API 及其生成的 API 类进行交互(例如 IAccessible)
除了基础窗口对象之外,NVDA 还附带了一系列窗口对象,用于处理诸如 Excel 之类的操作。
这些对象不一定使用无障碍 API 来完成各种任务——需要无障碍 API 的对象被分组到相应的 API 目录中。
窗口对象主要依靠诸如窗口句柄和类名之类的属性来告知 NVDA 它正在处理的控件类型。
紧邻 NVDA 基础对象之上的另一个类(或一组类)是行为。
行为类包括表格单元格的表示(并因此提供表格导航命令)、进度条、对话框、可编辑文本等等。
这些类旨在使 NVDA 能够以特定方式运行 —— 例如播放进度条蜂鸣声。
我可以在以后的文章中详细讨论行为类;目前,我们将重点介绍以下其他类。
译者注:
API 类
现在介绍第二层:API 类。
这些类代表各种 API 及其支持的控件,包括 MSAA/IAccessible (NVDAObjects.IAccessible.IAccessible
)、
Java Access Bridge (NVDAObjects.JAB.JAB
) 和 UI Automation (NVDAObjects.UIA.UIA
)。
如上所述,这些类需要窗口句柄才能正常运行,因此它们都继承自窗口基类。
API 类的核心作用是通过提供名称、角色、描述以及其他属性和方法的具体实现,来补充基本 NVDA 对象,并利用实际无障碍 API 的特性。
(需要注意的是,由于基本 NVDA 对象是抽象类,许多属性和方法会显示为"未实现")
定义完成后,API 类为 NVDA 提供了处理特定 API 支持的控件的方法,同时也为覆盖类(下文将详述)提供了常用服务。
举例来说,NVDA 获取控件标签(名称)的方式在 IAccessible 和 UIA 之间存在差异。
任何 UIA 类都可以直接使用 UIA 的方式查找控件标签,而无需自行定义实现方式
(因为基础 UIA NVDA 对象已经完成了这些基础工作;您可以在覆盖类中重新定义标签的获取方式)。
API 类通常不会被直接使用 —— 在大多数情况下,开发者会根据控件对应的 NVDA 对象类型从这些类中继承
(例如,处理 IAccessible 对象时,会创建一个从 IAccessible 基类继承的新类)。
直接使用这些 API 类的唯一场景是:当您需要编辑 NVDA 源代码,为这些类添加可被继承类或覆盖类使用的新功能。
覆盖类
下一层(也是最后一层)是覆盖类,既有内置于 NVDA 的,也有由 NVDA 插件定义的,
其中许多插件源自具体的无障碍 API 类。
顾名思义,覆盖类是一种“自定义覆盖”,旨在帮助 NVDA 应对特定情况。
例如,名为 SysListView32
的覆盖类(派生自 IAccessible),在任务管理器等位置提供表格导航命令,
名为 SuggestionsList
的类(来自 UIA)会在 Windows 10,11 设置等应用中宣告建议计数。
NVDA 之所以非常灵活,是因为插件可以定义自己的覆盖类:
一个很好的例子就是在安装 Mozilla 应用增强插件后,NVDA 如何与各种 Mozilla 应用和控件协同工作。
从技术角度来说,覆盖类是一个自定义或扩展的 API 类,提供自定义的方法来执行某些操作。
NVDA 所关注的只是它能够完成其工作,例如宣告标签、响应事件以及执行各种操作,
而无需了解底层发生了什么,而这是 NVDA 对象的用户体验方面。
在代码层面,覆盖类可以扩展或替换基类提供的服务,
无论基类是 API 类、行为类,还是至少一个插件中的 NVDA 基类对象。
换句话说,覆盖类可以塑造 NVDA 呈现给用户的体验,如果插件定义了自己的覆盖类,则更是如此。
译者注:与覆盖类相关的开发文档是
Providing Custom NVDA Object Classes (release-2024.4.2)
示例:StationPlaylist 插件
为了展示覆盖类在塑造 NVDA 行为和用户体验方面的强大功能,
我们来看一下 StationPlaylist 插件,以及 NVDA 如何在歌曲列表中提供表格导航命令。
作为此插件的一部分,定义了一个 StationPlaylist Studio (splstudio
) 的应用模块,
该应用模块内部包含一个覆盖类,它提供了歌曲项的抽象视图。
该歌曲项类 (appModules.splstudio.SPLTrackItem
) 派生自一长串 IAccessible 类:
- SysListView32 列表项(
NVDAObjects.IAccessible.syslistview32.ListItem
),
为某些列表提供表格导航命令,由以下功能提供支持: - 伪表格行和导航(
NVDAObjects.behaviors.RowWithFakeNavigation
),
为不是真表格中的行,定义表格导航命令(这里指 NVDA 第一次能获取的部分,后续还可以动态加载更多)。
但这还不是全部: - 基本 IAccessible/MSAA 服务(
NVDAObjects.IAccessible.IAccessible
) - 窗口(
NVDAObjects.window.Window
) - 基本 NVDA 对象(
NVDAObjects.NVDAObject
)
最终,SPL 歌曲列表中的表格导航命令来自 sysListView32.ListItem
类。
但是,SPL 应用模块中的歌曲项类会覆盖某些用于获取列内容的方法,从而改变 NVDA 获取单元格内容的方式。
所有这些自定义行为都由插件中定义的单个覆盖类实现。
现在您看到了覆盖类的强大之处;给插件作者的忠告:您的代码会在一定程度上影响用户对 NVDA 的看法。
或者,简单地说,覆盖类的作用是自定义 NVDA 的行为以适应特定情况,而这一切都归功于抽象、继承和多态性。
译者注:
作者在这里列出了一堆继承的类,实际上该歌曲类直接继承了
sysListView32.ListItem
类。
其他列出的类,都是由ListItem
间接继承的。StationPlaylist 插件目前仍在维护。当前的维护者是 Joseph Lee,即本文作者。
特殊情况
最后,NVDA 如何处理“特殊情况”?这主要发生在 NVDA 对象的构造时。
首先,NVDA 将确定需要使用哪个 API 类。
然后,NVDA 将确定这是否是已知的对象
(通过调用 findOverlayClasses
来查找所选 API 的内置覆盖类),
接着询问代表该控件所属应用程序的应用程序模块,然后遍历已加载的全局插件,看看插件是否知道该对象
(对于应用程序模块和全局插件,将调用 chooseNVDAObjectOverlayClasses
)。
NVDA 还可以询问应用程序模块是否需要进一步自定义对象,例如使用不同的角色(event_NVDAObject_init
)。
如果生成的对象是一个覆盖类(而不是 API 类),则可以将其视为“特殊情况”,
否则将直接使用无障碍 API 提供的服务,因为生成的 NVDA 对象是一个纯 API 类(没有自定义化)。
总结
NVDA 对象系列文章共分为三部分,涵盖了 NVDA 对象的概念、工作原理、内部结构以及如何自定义。
本系列文章的关键点在于,NVDA 竭尽全力在各种无障碍 API 中提供通用的用户体验,并且始终考虑各种特殊情况。
从某种程度上来说,了解 NVDA 对象、其结构以及覆盖类的用途,是创建高效插件所需的最低知识要求,特别是在设计应用模块时。
希望这有所帮助。
欢迎提问和评论。
Cheers,
Joseph
评论与回复
- 评论中有人询问了是否还有类似的技术文章,作者回复了这是系列文章之一。
- 作者还提到之后可能会暂停更新,因为要忙于研究生学业。
原文为:“我将暂停发布《内幕故事》文章一段时间,以便专注于研究生学习。再过一个学期我就会知道是否可以在我的名字后面加上 "M.A."。”
译注
译自 Joseph Lee - The Inside Story of NVDA: API and overlay classes
(2023-01-13)
- 代码与开发文档均使用指向特定 git 提交的版本,这样可以保证不会因版本更新而失效。