Пользовательский интерфейс в Qt

QAccessible: основы доступности в Qt-приложениях

Введение

Доступность (accessibility, a11y) — обеспечение того, что приложением могут пользоваться люди с ограниченными возможностями: слабовидящие и незрячие пользователи через screen reader, люди с моторными нарушениями через клавиатурную навигацию вместо мыши, пользователи с нарушениями слуха через визуальные альтернативы звуковым сигналам. Qt предоставляет встроенную поддержку доступности через модуль QAccessible, интегрированный с большинством стандартных виджетов «из коробки» — но для кастомных виджетов и сложных сценариев нужна явная настройка. В статье разберём основы того, как Qt экспортирует информацию о доступности операционной системе.

Концепция

Каждая платформа (Windows — UI Automation/MSAA, macOS — NSAccessibility, Linux — AT-SPI) предоставляет собственный API, через который screen reader и другие вспомогательные технологии получают информацию о структуре интерфейса приложения — название элементов, их роль (кнопка, текстовое поле, список), состояние (нажата, отмечена, недоступна), и иерархию вложенности. QAccessible абстрагирует эти платформенные различия: Qt автоматически предоставляет базовую информацию о доступности для стандартных виджетов (текст кнопки, состояние чекбокса) без какого-либо дополнительного кода — но эта информация может быть улучшена через accessibleName/accessibleDescription, а для кастомных виджетов должна быть реализована явно через QAccessibleInterface.

Пример кода: QAccessible

#include <QApplication>
#include <QPushButton>
#include <QLineEdit>
#include <QLabel>
#include <QVBoxLayout>

class SearchWidget : public QWidget
{
    Q_OBJECT
public:
    explicit SearchWidget(QWidget *parent = nullptr) : QWidget(parent)
    {
        auto *searchField = new QLineEdit(this);
        // accessibleName — то, что прочитает screen reader, может отличаться
        // от видимого текста (например, если рядом нет текстовой подписи-label)
        searchField->setAccessibleName("Поле поиска по каталогу товаров");
        searchField->setPlaceholderText("Введите название товара...");

        auto *searchButton = new QPushButton("🔍", this); // только иконка, без текста
        // Без accessibleName screen reader прочитал бы только символ эмодзи,
        // что бесполезно для пользователя — нужно явное описание действия
        searchButton->setAccessibleName("Выполнить поиск");
        searchButton->setAccessibleDescription(
            "Запускает поиск товаров по тексту, введённому в поле поиска");

        auto *resultsLabel = new QLabel(this);
        resultsLabel->setAccessibleName("Результаты поиска");

        auto *layout = new QVBoxLayout(this);
        layout->addWidget(searchField);
        layout->addWidget(searchButton);
        layout->addWidget(resultsLabel);

        connect(searchButton, &QPushButton::clicked, [searchField, resultsLabel]() {
            resultsLabel->setText("Найдено товаров: 12 по запросу \"" + searchField->text() + "\"");
            // Уведомление screen reader об изменении текста — важно для
            // динамически обновляемого контента (см. подробности в статье 205)
            QAccessibleEvent event(resultsLabel, QAccessible::NameChanged);
            QAccessible::updateAccessibility(&event);
        });
    }
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    SearchWidget widget;
    widget.show();
    return app.exec();
}

#include "main.moc"

Пояснения к коду

setAccessibleName() на searchField задаёт текст, который screen reader произнесёт при попадании фокуса на это поле — он может содержать более подробную информацию, чем видимый placeholder, который сам по себе не всегда корректно озвучивается как полноценная подпись поля во всех вспомогательных технологиях. Для кнопки с одним символом-эмодзи вместо текста (searchButton) явный accessibleName критичен — без него у пользователя screen reader не будет никакой осмысленной информации о назначении этой кнопки, что делает интерфейс полностью непригодным для использования без зрячего контроля.

Вызов QAccessible::updateAccessibility() с событием NameChanged после обновления текста resultsLabel — явное уведомление подсистемы доступности о том, что содержимое элемента изменилось динамически, в ответ на действие пользователя; без этого уведомления screen reader может не заметить изменение и не сообщить пользователю о появлении новых результатов поиска.

Подводные камни

  • Расчёт на то, что видимый текст автоматически становится доступным для screen reader без явной настройки. Для большинства стандартных текстовых элементов (QLabel, QPushButton с текстом) это действительно так — Qt автоматически использует видимый текст как имя для доступности. Но для виджетов без текста (иконки, кастомная графика) или с неинформативным текстом ("🔍") это предположение неверно, и явный accessibleName обязателен.
  • Изменение динамического контента без уведомления через QAccessible::updateAccessibility(). Screen reader, в отличие от зрячего пользователя, не «видит» визуальное изменение текста на экране автоматически — без явного программного уведомления об изменении он продолжит сообщать устаревшую информацию или вовсе не заметит изменения, пока пользователь не переместит фокус заново на этот элемент.
  • Использование accessibleDescription как замены, а не дополнения к accessibleName. accessibleName — короткое, основное обозначение элемента (что это), accessibleDescription — дополнительный, более подробный контекст (как это используется или для чего) — путаница между их назначением приводит к избыточно длинному или, наоборот, недостаточно информативному озвучиванию элемента screen reader’ом.
  • Тестирование доступности только зрячим разработчиком без реального screen reader. Визуальная проверка интерфейса не выявляет проблем доступности — единственный надёжный способ убедиться, что приложение действительно доступно, это протестировать его с реальным screen reader (NVDA на Windows, VoiceOver на macOS, Orca на Linux), что подробнее разбирается в статье 206 этого цикла.