Введение
Стандартные виджеты Qt автоматически интегрированы с системой доступности, но кастомные виджеты, нарисованные через переопределённый paintEvent() (как разбиралось в основном цикле статей про Qt Widgets), не предоставляют никакой информации screen reader без явной реализации. В статье построим полноценную поддержку доступности для кастомного виджета — кругового индикатора прогресса из предыдущего цикла статей про UI — через QAccessibleInterface.
Концепция
Для кастомного виджета, у которого нет встроенной поддержки доступности, нужно реализовать собственный класс, наследующий QAccessibleInterface (или, что чаще удобнее, QAccessibleWidget — облегчённую базовую реализацию для виджетов), переопределяющий методы, сообщающие screen reader роль элемента (role()), его текущее состояние (state()) и текстовое представление (text()). Эта реализация затем регистрируется через QAccessible::installFactory() — фабричную функцию, которую Qt вызывает каждый раз, когда подсистема доступности запрашивает интерфейс доступности для конкретного объекта.
Пример кода: Поддержка screen reader
// circularprogressbar.h — кастомный виджет из основного цикла, дополненный для доступности
#pragma once
#include <QWidget>
#include <QAccessibleWidget>
class CircularProgressBar : public QWidget
{
Q_OBJECT
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
explicit CircularProgressBar(QWidget *parent = nullptr) : QWidget(parent)
{
setMinimumSize(100, 100);
setFocusPolicy(Qt::TabFocus); // делаем виджет доступным для клавиатурной навигации
}
int value() const { return m_value; }
void setValue(int value)
{
value = qBound(0, value, 100);
if (m_value == value) return;
m_value = value;
emit valueChanged(value);
update();
// Уведомляем подсистему доступности об изменении значения
QAccessibleValueChangeEvent event(this, value);
QAccessible::updateAccessibility(&event);
}
signals:
void valueChanged(int value);
protected:
void paintEvent(QPaintEvent *event) override; // реализация из основного цикла
private:
int m_value = 0;
};
// circularprogressbaraccessible.h — интерфейс доступности для кастомного виджета
#pragma once
#include <QAccessibleWidget>
#include "circularprogressbar.h"
class CircularProgressBarAccessible : public QAccessibleWidget
{
public:
explicit CircularProgressBarAccessible(QWidget *widget)
: QAccessibleWidget(widget, QAccessible::ProgressBar) {}
QString text(QAccessible::Text textType) const override
{
auto *progressBar = static_cast<CircularProgressBar *>(widget());
if (textType == QAccessible::Name) {
return "Индикатор прогресса загрузки";
}
if (textType == QAccessible::Value) {
return QString::number(progressBar->value()) + "%";
}
return QAccessibleWidget::text(textType);
}
QAccessible::State state() const override
{
QAccessible::State state = QAccessibleWidget::state();
// Дополнительных специфичных флагов состояния для этого виджета не требуется,
// базовая реализация уже сообщает focusable/visible корректно
return state;
}
};
// Фабричная функция, регистрируемая в подсистеме доступности
QAccessibleInterface *circularProgressBarAccessibleFactory(const QString &classname, QObject *object)
{
if (classname == QLatin1String("CircularProgressBar") && object && object->isWidgetType()) {
return new CircularProgressBarAccessible(static_cast<QWidget *>(object));
}
return nullptr;
}
// main.cpp — регистрация фабрики при старте приложения
#include <QApplication>
#include <QAccessible>
#include "circularprogressbaraccessible.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QAccessible::installFactory(circularProgressBarAccessibleFactory);
CircularProgressBar progressBar;
progressBar.setValue(45);
progressBar.show();
return app.exec();
}
Пояснения к коду
CircularProgressBarAccessible наследует QAccessibleWidget, который уже предоставляет разумную базовую реализацию большинства методов интерфейса доступности (геометрия, видимость, фокусируемость), требуя переопределения только специфичных для конкретного виджета аспектов — text() возвращает осмысленное имя («Индикатор прогресса загрузки») и текущее значение в процентах, что screen reader озвучит при попадании фокуса на этот элемент. Указание роли QAccessible::ProgressBar в конструкторе сообщает вспомогательным технологиям, что это семантически именно индикатор прогресса, а не произвольный графический элемент — это влияет на то, как именно screen reader представит элемент пользователю (например, объявит «индикатор прогресса, 45 процентов» вместо невнятного «неизвестный элемент интерфейса»).
circularProgressBarAccessibleFactory() — фабричная функция, которую Qt вызывает для каждого объекта, для которого запрашивается интерфейс доступности; проверка classname == "CircularProgressBar" гарантирует, что собственная реализация подставляется именно для экземпляров этого конкретного класса, не затрагивая доступность остальных виджетов приложения. Регистрация через QAccessible::installFactory() должна произойти до создания и показа виджетов, для которых эта фабрика должна сработать.
Подводные камни
- Забытая регистрация фабрики (
installFactory) до создания виджета. Если фабрика регистрируется после того, как экземпляр виджета уже создан и (что важнее) подсистема доступности уже успела один раз запросить и закэшировать для него интерфейс по умолчанию, кастомная реализация может не подключиться к уже существующему экземпляру — порядок инициализации важен, и регистрация должна происходить как можно раньше вmain(). setFocusPolicy(Qt::TabFocus)забыт для кастомного виджета, который по умолчанию (как большинство простыхQWidget-наследников без явной настройки) не участвует в обходе фокуса по Tab — даже при полностью корректной реализации интерфейса доступности, если пользователь, навигирующий только с клавиатуры (включая пользователей screen reader, которые почти никогда не используют мышь), физически не может перевести фокус на этот элемент через Tab, вся проделанная работа по доступности оказывается недостижимой для него.- Сравнение строкового имени класса (
classname == "CircularProgressBar") без учёта пространств имён или возможного переименования класса при рефакторинге. Если класс позже перемещается в namespace или переименовывается, а строка в фабричной функции не обновляется синхронно, фабрика тихо перестаёт срабатывать для этого класса без какой-либо явной ошибки компиляции — несоответствие обнаружится только при ручном тестировании с screen reader. - Отсутствие уведомления о значении через
QAccessibleValueChangeEventпри изменении прогресса — без этого вызова (как показано вsetValue()) screen reader, уже сфокусированный на этом элементе ранее, не узнает об изменении значения автоматически и продолжит сообщать устаревший процент при повторных запросах пользователя, аналогично проблемеNameChanged, разобранной в предыдущей статье.