QWidget 应用程序的可访问性

介绍

我们会聚焦于 Qt 可访问接口 QAccessibleInterface 和如何使应用程序可访问。

基于 QWidget 的应用程序中的可访问性

当我们与为残疾人设计的技术通信时,需要以他们可理解的方式来描述 Qt 用户界面。Qt 应用程序使用 QAccessibleInterface 去暴露单个 UI 元素的有关信息。目前,Qt 提供对其 Widget 和 Widget审查 部件的支持 (如:滑块手柄),但接口也可以被实现为任何 QObject 若有必要。 QAccessible 包含描述 UI 的枚举。我们将在本文档编制过程中审查枚举。

UI 的结构被表示成树为 QAccessibleInterface 子类。这经常是构成应用程序 UI 的 QWidgets 的层次结构的镜像。

服务器通知客户端透过 updateAccessibility() 通过发送事件对接对象改变,客户端注册以接收事件。可用事件的定义通过 QAccessible::Event 枚举。然后,客户端可以查询事件生成对象透过 QAccessible::queryAccessibleInterface ().

成员和枚举在 QAccessible 用于描述可访问对象:

  • Role :描述对象在用户界面中所担任的角色 (如:若它是窗口、文本编辑或表格单元格)。
  • Relation :描述在对象层次结构中对象之间的关系。
  • State :对象可以在许多不同状态下 (状态范例,包括:对象是否被禁用,是否已聚焦或是否提供弹出菜单)。

客户端还有一些可能获取对象内容 (如:按钮文本);对象提供字符串,定义通过 QAccessible::Text 枚举,给出有关内容的信息。

可访问对象树

As mentioned, a tree structure is built from the accessible objects of an application. By navigating through the tree, the clients can access all elements in the UI. Object relations give clients information about the UI. For instance, a slider handle is a child of the slider to which it belongs. QAccessible::Relation describes the various relationships the clients can ask objects for.

Note that there are no direct mapping between the Qt QObject tree and the accessible object tree. For instance, scroll bar handles are accessible objects but are not widgets or objects in Qt.

AT-Clients have access to the accessibility object tree through the root object in the tree, which is the QApplication . They can navigate the tree with the QAccessibleInterface::parent (), QAccessibleInterface::childCount () 和 QAccessibleInterface::child () 函数。

Qt provides accessible interfaces for its widgets and for Qt Quick Controls. Interfaces for any QObject subclass can be requested through QAccessible::queryInterface(). A default implementation is provided if a more specialized interface is not defined. An AT-Client cannot acquire an interface for accessible objects that do not have an equivalent QObject , e.g., scroll bar handles, but they appear as normal objects through interfaces of parent accessible objects, e.g., you can query their relationships with QAccessibleInterface::relations ().

To illustrate, we present an image of an accessible object tree. Beneath the tree is a table with examples of object relationships.

The labels in top-down order are: the QAccessibleInterface class name, the widget for which an interface is provided, and the Role of the object. The Position, PageLeft and PageRight correspond to the slider handle, the slider groove left and the slider groove right, respectively. These accessible objects do not have an equivalent QObject .

源对象 目标对象 Relation
Slider Indicator Controller
Indicator Slider Controlled
Slider 应用程序 Ancestor
应用程序 Slider Child
PushButton Indicator Sibling

静态 QAccessible 函数

可访问性的管理是通过 QAccessible 的静态函数,我们很快会审查。它们产生 QAccessible interfaces, build the object tree, and initiate the connection with MSAA or the other platform specific technologies. If you are only interested in learning how to make your application accessible, you can safely skip over this section to 实现可访问性 .

The communication between clients and the server is initiated when setRootObject() is called. This is done when the QApplication instance is instantiated and you should not have to do this yourself.

QObject calls updateAccessibility() , clients that are listening to events are notified of the change. The function is used to post events to the assistive technology, and accessible events are posted by updateAccessibility() .

queryAccessibleInterface() returns accessible interfaces for QObject s. All widgets in Qt provide interfaces; if you need interfaces to control the behavior of other QObject subclasses, you must implement the interfaces yourself, although the QAccessibleObject convenience class implements parts of the functionality for you.

The factory that produces accessibility interfaces for QObjects is a function of type QAccessible::InterfaceFactory . It is possible to have several factories installed. The last factory installed will be the first to be asked for interfaces. queryAccessibleInterface() uses the factories to create interfaces for QObject s. Normally, you need not be concerned about factories because you can implement plugins that produce interfaces. We will give examples of both approaches later.

实现可访问性

To provide accessibility support for a widget or other user interface element, you need to implement the QAccessibleInterface and distribute it in a QAccessiblePlugin . It is also possible to compile the interface into the application and provide a QAccessible::InterfaceFactory for it. The factory can be used if you link statically or do not want the added complexity of plugins. This can be an advantage if you, for instance, are delivering a 3-rd party library.

All widgets and other user interface elements should have interfaces and plugins. If you want your application to support accessibility, you will need to consider the following:

  • Qt already implements accessibility for its own widgets. We therefore recommend that you use Qt widgets where possible.
  • A QAccessibleInterface needs to be implemented for each element that you want to make available to accessibility clients.
  • You need to send accessibility events from the custom user interface elements that you implement.

In general, it is recommended that you are somewhat familiar with MSAA, which Qt's accessibility support originally was built for. You should also study the enum values of QAccessible , which describe the roles, actions, relationships, and events that you need to consider.

Note that you can examine how Qt's widgets implement their accessibility. One major problem with the MSAA standard is that interfaces are often implemented in an inconsistent way. This makes life difficult for clients and often leads to guesswork on object functionality.

It is possible to implement interfaces by inheriting QAccessibleInterface and implementing its pure virtual functions. In practice, however, it is usually preferable to inherit QAccessibleObject or QAccessibleWidget , which implement part of the functionality for you. In the next section, we will see an example of implementing accessibility for a widget by inheriting the QAccessibleWidget 类。

AccessibleObject 和 QAccessibleWidget 方便类

When implementing an accessibility interface for widgets, one would as a rule inherit QAccessibleWidget , which is a convenience class for widgets. Another available convenience class, which is inherited by QAccessibleWidget , is the QAccessibleObject , which implements part of the interface for QObjects.

QAccessibleWidget provides the following functionality:

  • It handles the navigation of the tree and hit testing of the objects.
  • It handles events, roles, and actions that are common for all QWidget
  • It handles action and methods that can be performed on all widgets.
  • It calculates bounding rectangles with rect() .
  • It gives text() strings that are appropriate for a generic widget.
  • 它设置 states that are common for all widgets.

QAccessibleWidget 范例

Instead of creating a custom widget and implementing an interface for it, we will show how accessibility is implemented for one of Qt's standard widgets: QSlider . The accessible interface, QAccessibleSlider, inherits from QAccessibleAbstractSlider, which in turn inherits QAccessibleWidget . You do not need to examine the QAccessibleAbstractSlider class to read this section. If you want to take a look, the code for all of Qt's accessible interfaces are found in qtbase/src/widgets/accessible. Here is the QAccessibleSlider's constructor:

QAccessibleSlider::QAccessibleSlider(QWidget *w)
: QAccessibleAbstractSlider(w)
{
    Q_ASSERT(slider());
    addControllingSignal(QLatin1String("valueChanged(int)"));
}
					

The slider is a complex control that functions as a Controller for its accessible children. This relationship must be known by the interface (for parent() , child() and relations() ). This can be done using a controlling signal, which is a mechanism provided by QAccessibleWidget . We do this in the constructor:

The choice of signal shown is not important; the same principles apply to all signals that are declared in this way. Note that we use QLatin1String to ensure that the signal name is correctly specified.

When an accessible object is changed in a way that users need to know about, it notifies clients of the change by sending them an event via the accessible interface. This is how QSlider calls updateAccessibility() to indicate that its value has changed:

void QAbstractSlider::setValue(int value)
    ...
    QAccessibleValueChangeEvent event(this, d->value);
    QAccessible::updateAccessibility(&event);
    ...
}
					

Note that the call is made after the value of the slider has changed because clients may query the new value immediately after receiving the event.

The interface must be able to calculate bounding rectangles of itself and any children that do not provide an interface of their own. The QAccessibleSlider has three such children identified by the private enum, SliderElements , which has the following values: PageLeft (the rectangle on the left hand side of the slider handle), PageRight (the rectangle on the right hand side of the handle), and Position (the slider handle). Here is the implementation of 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);
    }
    ...
					

The first part of the function, which we have omitted, uses the current style to calculate the slider handle's bounding rectangle; it is stored in srect . Notice that child 0, covered in the default case in the above code, is the slider itself, so we can simply return the QSlider bounding rectangle obtained from the superclass, which is effectively the value obtained from QAccessibleWidget::rect ().

    QPoint tp = slider()->mapToGlobal(QPoint(0,0));
    return QRect(tp.x() + rect.x(), tp.y() + rect.y(), rect.width(), rect.height());
}
					

Before the rectangle is returned it must be mapped to screen coordinates.

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() function returns a pointer to the interface's QSlider . Some values are left for the superclass's implementation. Not all values are appropriate for all accessible objects, as you can see for QAccessible::Value case. You should just return an empty string for those values where no relevant text can be provided.

实现为 role() function is straightforward:

QAccessible::Role QAccessibleSlider::role(int child) const
{
    switch (child) {
    case PageLeft:
    case PageRight:
        return PushButton;
    case Position:
        return Indicator;
    default:
        return Slider;
    }
}
					

The role function should be reimplemented by all objects and describes the role of themselves and the children that do not provide accessible interfaces of their own.

Next, the accessible interface needs to return the states that the slider can be in. We look at parts of the state() implementation to show how just a few of the states are handled:

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;
}
					

The superclass implementation of state() ,使用 QAccessibleInterface::state () implementation. We simply need to disable the buttons if the slider is at its minimum or maximum.

We have now exposed the information we have about the slider to the clients. For the clients to be able to alter the slider - for example, to change its value - we must provide information about the actions that can be performed and perform them upon request. We discuss this in the next section.

处理来自客户端的动作请求

Applications can expose actions, which can be invoked by the client. In order to support actions in an object, inherit the QAccessibleActionInterface .

Interactive elements should expose functionality triggered by mouse interaction, for example. A button should, for example, implement a click action.

Setting the focus is another action that should be implemented for widgets that accept receive the focus.

You need to re-implement actionNames() to return a list of all actions that the object supports. This list should not be localized.

There are two functions that give information about the actions that must return localized strings: localizedActionName() and localizedActionDescription() . These functions can be used by the client to present the actions to the user. In general, the name should be concise and only consist of a single word, such as "press".

There is a list of standard action names and localizations available that should be used when the action fits. This makes it easier for clients to understand the semantics, and Qt will try to expose them correctly on the different platforms.

Of course the action also needs a way to be triggered. doAction() should invoke the action as advertised by name and description.

To see examples on how to implement actions and methods, you could examine the implementations for Qt's standard widgets such as QAccessiblePushButton.

实现可访问插件

In this section we will explain the procedure of implementing accessible plugins for your interfaces. A plugin is a class stored in a shared library that can be loaded at run-time. It is convenient to distribute interfaces as plugins since they will only be loaded when required.

Creating an accessible plugin is achieved by inheriting QAccessiblePlugin , defining the supported class names in the plugin's JSON description and reimplementing create() from QAccessiblePlugin .pro file must be altered to use the plugin template, and the library containing the plugin must be placed on a path where Qt searches for accessible plugins.

We will go through the implementation of SliderPlugin , which is an accessible plugin that produces the QAccessibleSlider interface from the QAccessibleWidget 范例 . We start with the key() 函数:

QStringList SliderPlugin::keys() const
{
    return QStringList() << QLatin1String("QSlider");
}
					

We simply need to return the class name of the single interface our plugin can create an accessible interface for. A plugin can support any number of classes; just add more class names to the string list. We move on to the 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;
}
					

We check whether the interface requested is for QSlider ; if it is, we create and return an interface for it. Note that 对象 will always be an instance of classname . You must return 0 if you do not support the class. updateAccessibility() checks with the available accessibility plugins until it finds one that does not return 0.

Finally, you need to include macros in the cpp file:

    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.Accessibility.SliderPlugin" FILE "slider.json")
					

Q_PLUGIN_METADATA macro exports the plugin in the SliderPlugin class into the acc_sliderplugin library. The first argument is the plugins IID and the second is an optional json file which holds metadata information for the plugin. For more information on plugins, you can consult the plugins 概述文档 .

It does not matter if you need the plugin to be statically or dynamically linked with the application.

实现接口工厂

If you do not want to provide plugins for your accessibility interfaces, you can use an interface factory ( QAccessible::InterfaceFactory ), which is the recommended way to provide accessible interfaces in a statically-linked application.

A factory is a function pointer for a function that takes the same parameters as QAccessiblePlugin 's create() - a QString QObject . It also works the same way. You install the factory with the installFactory() function. We give an example of how to create a factory for the QAccessibleSlider interface:

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);
    ...
}