QWidget应用程序的无障碍访问
简介
我们将重点关注Qt无障碍接口QAccessibleInterface以及如何使应用程序无障碍。
基于QWidget的应用程序的无障碍访问
当我们与辅助技术通信时,我们需要以他们能理解的方式描述Qt的用户界面。Qt应用程序使用QAccessibleInterface来暴露有关单个UI元素的信息。目前,Qt为其小工具和小部件部分提供支持,例如滑块句柄,但接口也可以为实现任何QObject(如果必要)而实现。QAccessible包含描述UI的枚举。在本文档的过程中,我们将检查枚举。
UI的结构表示为QAccessibleInterface子类的树。这通常是组成应用程序UI的QWidgets层次结构的镜像。
服务器通过updateAccessibility()事件发送事件来通知客户端对象的变化,客户端注册接收这些事件。可用的事件由QAccessible::Event枚举定义。然后,客户端可以通过QAccessible::queryAccessibleInterface()查询生成事件的对象。
QAccessible中的成员和枚举用于描述无障碍对象
- 角色:描述对象在用户界面中扮演的角色,例如它是窗口、文本编辑还是表格中的单元格。
- 关系:描述对象层次结构中对象之间的关系。
- 状态:对象可以处于许多不同的状态。状态示例包括对象是否禁用、是否具有焦点或是否提供弹出菜单。
客户端还有一些获取对象内容的可能性,例如按钮的文本;对象提供由QAccessible::Text枚举定义的字符串,这些字符串提供了有关内容的信息。
无障碍对象树
如前所述,由应用程序的无障碍对象构建树结构。通过在树中导航,客户端可以访问UI中的所有元素。对象关系为客户提供有关UI的信息。例如,滑块句柄是其所属滑块的子代。QAccessible::Relation描述了客户端可以请求对象的各个关系。
请注意,Qt QObject树与无障碍对象树之间没有直接的映射。例如,滚动条句柄是可访问对象,但不是小工具或Qt中的对象。
AT-Clients可以通过树中的根对象访问可访问性对象树,该根对象是QApplication。它们可以使用QAccessibleInterface::parent(),QAccessibleInterface::childCount() 和 QAccessibleInterface::child() 函数来遍历树。
Qt为其小部件和Qt Quick控件提供了可访问接口。可以通过 QAccessible::queryInterface() 要求任何QObject子类的接口。如果没有定义更加专门的接口,将提供一个默认实现。AT-Client无法获取没有对应QObject的可访问对象接口,例如滚动条句柄,但它们可以通过父可访问对象的接口显示为普通对象,例如,您可以通过QAccessibleInterface::relations()查询它们的关系。
为了说明,我们提供了一个可访问对象树的图像。在树下面有一个表格,其中包含对象关系的示例。
从上到下的标签顺序是:QAccessibleInterface类名、提供接口的小部件以及对象的Role。位置、PageLeft 和 PageRight 分别对应滑动块句柄、滑动槽左和滑动槽右。这些可访问对象没有对应的QObject。
源对象 | 目标对象 | 关系 |
---|---|---|
滑动块 | 指示器 | 控制器 |
指示器 | 滑动块 | 受控对象 |
滑动块 | 应用程序 | 祖先 |
应用程序 | 滑动块 | 子对象 |
推按钮 | 指示器 | 兄弟 |
静态 QAccessible 函数
可访问性由QAccessible的静态函数管理,这些函数我们稍后将进行探讨。这些函数生成QAccessible接口、构建对象树以及与MSAA或其他特定平台技术的连接。如果您只是想学习如何使您的应用程序具有可访问性,您可以安全地跳过这部分内容,直接跳到实现可访问性。
当调用setRootObject()时,客户端和服务器的通信开始。这是在实例化QApplication实例时完成的,您不需要自己这样做。
QObject调用updateAccessibility()时,正在监听事件的客户端会收到更改通知。此函数用于向辅助技术服务器发送事件,由updateAccessibility()发送可访问事件。
queryAccessibleInterface()返回QObject的可访问接口。Qt中的所有小部件都提供了接口;如果您需要控制其他QObject子类的行为的接口,您必须自己实现接口,尽管QAccessibleObject方便类为您实现了部分功能。
为QObjects生产辅助接口的工厂是类型 QAccessible::InterfaceFactory 的函数。可以安装多个工厂。最后安装的工厂将是首先询问接口的工厂。 queryAccessibleInterface() 使用这些工厂为 QObject 创建接口。通常,你无需关心工厂,因为你可以实现产生接口的插件。我们稍后将给出两种方法的示例。
实现辅助功能
为了向小部件或其他用户界面元素提供辅助功能支持,你需要实现 QAccessibleInterface 并将其分发给 QAccessiblePlugin。也可以将接口编译到应用程序中并为其提供 QAccessible::InterfaceFactory。如果静态链接或不想增加插件带来的复杂性,则可以使用此工厂。如果你正在交付第三方库,这可以是一个优势。
所有的小部件和其他用户界面元素都应该具有接口和插件。如果你想让应用程序支持辅助功能,你需要考虑以下内容
- Qt 已经实现了其小部件的辅助功能。因此,我们建议在可能的情况下使用 Qt 小部件。
- 需要为每个你想使辅助功能客户端可以访问的元素实现一个 QAccessibleInterface。
- 你需要从你实现的自定义用户界面元素中发送辅助功能事件。
通常,建议你对 MSAA(Qt 的辅助功能支持最初是为它而构建的)有一些了解。你还应该研究 QAccessible 的枚举值,它描述了需要考虑的角色、操作、关系和事件。
请注意,你可以检查 Qt 小部件如何实现它们的辅助功能。MSAA 标准的一个主要问题是接口通常以不一致的方式实现。这为客户的生活带来困难,并经常导致对对象功能进行猜测。
可以通过继承 QAccessibleInterface 并实现其纯虚函数来实现接口。然而,在实践中,通常最好是继承 QAccessibleObject 或 QAccessibleWidget,这些类为您实现了部分功能。在下一节中,我们将看到一个通过继承 QAccessibleWidget 类来为小部件实现辅助功能的示例。
QAccessibleObject 和 QAccessibleWidget 便捷类
当为小部件实现辅助功能接口时,通常会继承 QAccessibleWidget,这是一个为小部件提供的便捷类。另一个可用的便捷类是 QAccessibleObject,它继承自 QAccessibleWidget,实现了 QObject 的部分接口。
QAccessibleWidget 提供以下功能
- 它处理树的导航和对象的击中测试。
- 它处理所有 QWidget 共同的事件、角色和操作。
- 它处理可以在所有小部件上执行的操作和方法。
- 它使用 rect() 计算边界矩形。
- 它提供适合通用小部件的 text() 字符串。
- 它设置了所有小部件的通用状态。
QAccessibleWidget 示例
我们不会创建一个自定义小部件并为它实现一个接口,而是展示Qt标准小部件中的可访问性是如何实现的:QSlider。可访问接口 QAccessibleSlider 继承自 QAccessibleAbstractSlider,而 QAccessibleAbstractSlider 则继承自 QAccessibleWidget。阅读本节不需要检查 QAccessibleAbstractSlider 类。如果您想查看代码,所有Qt可访问接口的代码都在 qtbase/src/widgets/accessible 目录下。以下是 QAccessibleSlider 的构造函数
QAccessibleSlider::QAccessibleSlider(QWidget *w) : QAccessibleAbstractSlider(w) { Q_ASSERT(slider()); addControllingSignal(QLatin1String("valueChanged(int)")); }
滑块是一个复杂的控件,作为其可访问子控件的控制器。接口必须知道这种关系(对于 parent(),child()和 relations()))。这可以通过使用控制信号来实现,这是由 QAccessibleWidget 提供的一种机制。我们就在构造函数中这样做
显示的信号选择并不重要;相同的原理适用于以这种方式声明的所有信号。请注意,我们使用 QLatin1String 确保指定了正确的信号名称。
当一个可访问对象以用户需要知道的方式改变时,它会通过可访问接口向客户端发送事件来通知他们更改。这就是 QSlider 如何调用 updateAccessibility() 来指示其值已更改。
void QAbstractSlider::setValue(int value) ... QAccessibleValueChangeEvent event(this, d->value); QAccessible::updateAccessibility(&event); ... }
请注意,在滑块的值更改后调用此调用,因为客户端可能在接收到事件后立即查询新值。
接口必须能够计算其自身和未提供自己接口的任何子控件的边界矩形。QAccessibleSlider 有三个这样的子控件,通过私有的枚举 SliderElements
标识,具有以下值:PageLeft
(滑块句柄左侧的矩形)、PageRight
(句柄右侧的矩形)和 Position
(滑块句柄)。以下是 rect() 的实现
QRect QAccessibleSlider::rect(int child) const { ... switch (child) { case PageLeft: if (slider()->orientation() == Qt::Vertical) rect = QRect(0, 0, slider()->width(), srect.y()); else rect = QRect(0, 0, srect.x(), slider()->height()); break; case Position: rect = srect; break; case PageRight: if (slider()->orientation() == Qt::Vertical) rect = QRect(0, srect.y() + srect.height(), slider()->width(), slider()->height()- srect.y() - srect.height()); else rect = QRect(srect.x() + srect.width(), 0, slider()->width() - srect.x() - srect.width(), slider()->height()); break; default: return QAccessibleAbstractSlider::rect(child); } ...
该函数的第一部分,我们已省略,使用当前的 style 来计算滑块句柄的边界矩形;它存储在 srect
中。请注意,代码中默认情况下覆盖的子控件 0 是滑块本身,因此我们可以简单地返回从超级类获得 的 QSlider 边界矩形,这实际上是来自 QAccessibleWidget::rect 的值。
QPoint tp = slider()->mapToGlobal(QPoint(0,0)); return QRect(tp.x() + rect.x(), tp.y() + rect.y(), rect.width(), rect.height()); }
在返回矩形之前,必须将其映射到屏幕坐标。
QAccessibleSlider 必须重新实现 QAccessibleInterface::childCount(),因为它管理没有接口的孩子。
text() 函数返回滑块的 QAccessible::Text 字符串。
QString QAccessibleSlider::text(Text t, int child) const { if (!slider()->isVisible()) return QString(); switch (t) { case Value: if (!child || child == 2) return QString::number(slider()->value()); return QString(); case Name: switch (child) { case PageLeft: return slider()->orientation() == Qt::Horizontal ? QSlider::tr("Page left") : QSlider::tr("Page up"); case Position: return QSlider::tr("Position"); case PageRight: return slider()->orientation() == Qt::Horizontal ? QSlider::tr("Page right") : QSlider::tr("Page down"); } break; default: break; } return QAccessibleAbstractSlider::text(t, child); }
slider()
函数返回接口的 QSlider 指针。一些值留给超级类的实现。并不是所有值都适用于所有可访问对象,如 QAccessible::Value 案所示。对于无法提供相关文本的值,您只需返回一个空字符串即可。
role() 函数的实现很简单
QAccessible::Role QAccessibleSlider::role(int child) const { switch (child) { case PageLeft: case PageRight: return PushButton; case Position: return Indicator; default: return Slider; } }
所有对象都应该重新实现角色函数,它描述了该对象及其自身和未提供可访问接口的自定义子控件的角色。
接下来,可访问界面需要返回滑块可能处于的状态。我们查看 state()
实现的部分,以展示只是处理了少数几个状态。
QAccessible::State QAccessibleSlider::state(int child) const { const State parentState = QAccessibleAbstractSlider::state(0); ... switch (child) { case PageLeft: if (slider->value() <= slider->minimum()) state |= Unavailable; break; case PageRight: if (slider->value() >= slider->maximum()) state |= Unavailable; break; case Position: default: break; } return state; }
state() 的父类实现使用了 QAccessibleInterface::state() 实现。我们只需在滑块在其最小值或最大值时禁用按钮。
我们现在已经将关于滑块的信息暴露给客户端。为了让客户端能够更改滑块(例如,更改其值),我们必须在客户端请求时提供可执行的操作信息。我们将在下一节讨论这个问题。
处理来自客户端的操作请求
应用可以公开操作,客户端可以调用这些操作。为了支持对象中的操作,需要继承 QAccessibleActionInterface。
交互元素应该公开由鼠标交互触发的功能,例如。例如,按钮应该实现点击操作。
对于可以接收焦点的小部件,还需要实现设置焦点这一操作。
你需要重新实现 actionNames(),返回支持的所有操作列表。此列表不应该被本地化。
有两个函数提供有关必须返回本地化字符串的操作信息:localizedActionName() 和 localizedActionDescription()。这些函数可以由客户端用来向用户展示这些操作。通常,名称应该简短,只包含一个单词,例如 "press"。
当操作符合条件时,应使用有标准操作名称和本地化列表,这有助于客户端理解语义,Qt也会尝试在不同平台上正确公开它们。
当然,操作也需要一种触发方式。 doAction() 应根据名称和描述激活操作。
要查看如何实现操作和方法的示例,可以检查 Qt 标准小部件(如 QAccessiblePushButton)的实现。
实现可访问插件
在本节中,我们将解释为您接口实现可访问插件的程序。插件是存储在共享库中的一个类,可以运行时加载。以插件的形式分发接口很方便,因为它们只有在需要时才会被加载。
通过继承 QAccessiblePlugin、在插件的 JSON 描述中定义支持的类名称,并重新实现 QAccessiblePlugin 中的 create() 方法,可以创建可访问插件。必须更改 .pro
文件以使用插件模板,并将包含插件的库放置在 Qt 搜索可访问插件的路径上。
我们将通过 SliderPlugin
的实现过程进行说明,它是一个可访问插件,从 QAccessibleWidget 示例 中生成 QAccessibleSlider 接口。我们从 key()
函数开始。
QStringList SliderPlugin::keys() const { return QStringList() << QLatin1String("QSlider"); }
我们只需返回插件可以为其创建可访问接口的单个接口的类名。插件可以支持任意数量的类;只需将更多类名添加到字符串列表中。接下来,我们将转向 create()
函数。
QAccessibleInterface *SliderPlugin::create(const QString &classname, QObject *object) { QAccessibleInterface *interface = 0; if (classname == QLatin1String("QSlider") && object && object->isWidgetType()) interface = new QAccessibleSlider(static_cast<QWidget *>(object)); return interface; }
我们检查请求的接口是否为 QSlider;如果是,则创建并返回相应的接口。请注意,object
将始终是 classname
的实例。如果不支持此类,必须返回 0。函数 updateAccessibility() 会检查可用的辅助功能插件,直到找到一个不返回 0 的插件。
最后,您需要在 cpp 文件中包含宏。
Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.Accessibility.SliderPlugin" FILE "slider.json")
Q_PLUGIN_METADATA 宏将 SliderPlugin
类中的插件导出到 acc_sliderplugin
库中。第一个参数是插件 IID,第二个参数是一个可选的 json 文件,其中包含关于插件的元数据信息。有关插件的更多详细信息,您可以参考插件的 概述文档。
您需要将插件静态或动态链接到应用程序中,这无所谓。
实现接口工厂
如果不想为您的辅助功能接口提供插件,可以使用接口工厂(QAccessible::InterfaceFactory),这是在静态链接应用程序中提供辅助功能接口的推荐方式。
工厂是一个函数指针,指向一个具有与 QAccessiblePlugin 的 create() 相同参数的函数 - 一个 QString 和一个 QObject。它也以相同的方式工作。您可以通过 installFactory() 函数安装工厂。以下是如何为 QAccessibleSlider
接口创建工厂的示例
QAccessibleInterface *sliderFactory(const QString &classname, QObject *object) { QAccessibleInterface *interface = 0; if (classname == QLatin1String("QSlider") && object && object->isWidgetType()) interface = new QAccessibleSlider(static_cast<QWidget *>(object)); return interface; } int main(int argc, char *argv[]) { QApplication app(argc, argv); QAccessible::installFactory(sliderFactory); ... }
相关类
启用 QML 项的辅助功能 | |
与辅助功能相关的枚举和静态函数 | |
在接口中实现支持可调用操作 | |
在对象中实现对可编辑文本的支持 | |
辅助功能通知的基类 | |
定义了一个接口,可公开有关可访问对象的详细信息 | |
为 QObjects 实现了 QAccessibleInterface 的部分 | |
提供用户界面元素辅助功能信息的插件的抽象基类 | |
实现支持选择处理的接口 | |
通知辅助功能框架,对象的状态已更改 | |
实现对 IAccessibleTable2 单元格接口的支持 | |
实现对 IAccessibleTable2 接口的支持 | |
表示表中、列表或树中单元格的增加或删除更改。如果更改影响了多行,firstColumn 和 lastColumn 将返回 -1。同样对于列,行函数可能返回 -1 | |
通知光标移动 | |
通知文本被插入 | |
实现对文本处理的辅助 | |
通知文本被删除 | |
发出对象文本选择变更的信号 | |
通知文本更改。这对于支持可编辑文本的辅助功能很有用,例如行编辑器。此事件在将新文本粘贴到选定的文段或编辑器覆盖模式下发生时发生 | |
描述了可访问对象的值变化 | |
实现了对操纵值的对象的支持 | |
实现了QWidgets的QAccessibleInterface |
© 2024 Qt公司有限公司。本文件中的文档贡献归其各自的所有者所有。本文件提供的文档已根据自由软件基金会发布的GNU自由文档许可1.3版本的条款许可。Qt及其相关标志是芬兰和/或其他国家的Qt公司商标。所有其他商标归其各自所有者所有。