Введение
Виджеты, рисующие своё содержимое полностью через QPainter (графики, диаграммы, кастомные индикаторы, canvas-подобные редакторы) представляют особую сложность для доступности — визуальная информация, переданная через цвет, форму, положение элементов на холсте, не имеет естественного текстового аналога, который мог бы быть автоматически извлечён Qt. В статье разберём подход к обеспечению доступности для содержательно сложного, полностью кастомно отрисованного виджета — простого линейного графика.
Концепция
Для графически сложных виджетов единого универсального решения не существует — обычно нужно явно спроектировать, как представить смысл визуальной информации в текстовой форме. Для графиков распространённый подход — предоставить через интерфейс доступности не визуальное описание линий и точек, а агрегированную сводку данных (минимум, максимум, тренд, ключевые точки) и, опционально, возможность навигации по отдельным точкам данных через клавиатуру с озвучиванием каждой при перемещении, превращая визуальный график в исследуемую программно структуру данных.
Пример кода: Доступность кастомных графических
// linechartwidget.h — кастомный график с поддержкой доступности
#pragma once
#include <QWidget>
#include <QVector>
#include <QAccessibleWidget>
class LineChartWidget : public QWidget
{
Q_OBJECT
public:
explicit LineChartWidget(QWidget *parent = nullptr) : QWidget(parent)
{
setFocusPolicy(Qt::StrongFocus);
}
void setData(const QVector<double> &values)
{
m_values = values;
m_selectedIndex = -1;
update();
}
const QVector<double> &values() const { return m_values; }
int selectedIndex() const { return m_selectedIndex; }
protected:
void paintEvent(QPaintEvent *event) override; // отрисовка линии графика
void keyPressEvent(QKeyEvent *event) override
{
if (m_values.isEmpty()) return;
if (event->key() == Qt::Key_Right) {
m_selectedIndex = qMin(m_selectedIndex + 1, m_values.size() - 1);
} else if (event->key() == Qt::Key_Left) {
m_selectedIndex = qMax(m_selectedIndex - 1, 0);
} else {
QWidget::keyPressEvent(event);
return;
}
update();
// Уведомляем подсистему доступности о смещении "виртуального" фокуса
// на конкретную точку данных — реализуется через изменение значения
QAccessibleValueChangeEvent valueEvent(this, m_values[m_selectedIndex]);
QAccessible::updateAccessibility(&valueEvent);
}
private:
QVector<double> m_values;
int m_selectedIndex = -1;
};
// linechartwidgetaccessible.h — интерфейс доступности с агрегированной сводкой и навигацией
#pragma once
#include <QAccessibleWidget>
#include "linechartwidget.h"
#include <numeric>
#include <algorithm>
class LineChartWidgetAccessible : public QAccessibleWidget
{
public:
explicit LineChartWidgetAccessible(QWidget *widget)
: QAccessibleWidget(widget, QAccessible::Graphic) {}
QString text(QAccessible::Text textType) const override
{
auto *chart = static_cast<LineChartWidget *>(widget());
const auto &values = chart->values();
if (textType == QAccessible::Name) {
if (values.isEmpty()) return "График без данных";
const double minVal = *std::min_element(values.begin(), values.end());
const double maxVal = *std::max_element(values.begin(), values.end());
const double avgVal = std::accumulate(values.begin(), values.end(), 0.0) / values.size();
const QString trend = values.last() > values.first() ? "восходящий" : "снижающийся";
return QString("Линейный график, %1 точек данных. Минимум: %2, максимум: %3, "
"среднее: %4. Общий тренд: %5. Используйте стрелки влево/вправо "
"для навигации по отдельным точкам данных.")
.arg(values.size()).arg(minVal).arg(maxVal).arg(avgVal, 0, 'f', 1).arg(trend);
}
if (textType == QAccessible::Value) {
const int idx = chart->selectedIndex();
if (idx < 0 || idx >= values.size()) return "Точка не выбрана";
return QString("Точка %1 из %2: значение %3")
.arg(idx + 1).arg(values.size()).arg(values[idx]);
}
return QAccessibleWidget::text(textType);
}
};
Пояснения к коду
LineChartWidget реализует клавиатурную навигацию по отдельным точкам данных через стрелки влево/вправо, изменяя m_selectedIndex — это превращает изначально чисто визуальный график в структуру, которую можно «обходить» программно, что является необходимым предварительным условием для предоставления её содержимого screen reader пошагово, точка за точкой, а не только как единую неделимую визуальную картинку.
LineChartWidgetAccessible::text() для роли Name формирует развёрнутую агрегированную сводку — не пытаясь описать каждую точку линии (что было бы избыточно при большом количестве данных), а сообщая ключевые статистические характеристики (минимум, максимум, среднее, общий тренд) сразу при попадании фокуса на график, и явно инструктирует пользователя о возможности более детальной навигации через стрелки. Для роли Value возвращается информация о конкретно выбранной в данный момент точке — это значение динамически меняется при каждом нажатии стрелки, что и озвучивается через событие QAccessibleValueChangeEvent, отправленное в keyPressEvent().
Подводные камни
- Описание сложного графика единственной длинной строкой при каждом попадании фокуса может быть избыточно многословным и утомительным при частом повторном фокусировании на одном и том же графике — для графиков, к которым пользователь возвращается неоднократно в рамках одной сессии работы, возможно стоит предусмотреть более компактное повторное объявление (например, сокращённую версию при повторном фокусе той же сессии), хотя это усложняет реализацию и не всегда оправдано для типичных сценариев использования.
- Агрегированная статистика (минимум/максимум/среднее), не отражающая реально важный для конкретной задачи аспект данных. Для одних графиков важен общий тренд, для других — конкретные аномалии или выбросы, для третьих — сравнение с пороговым значением — универсальная сводка из примера хороша как базовое решение, но для специфичных по смыслу графиков (например, индикатор температуры с критичным порогом) стоит адаптировать содержание сводки под конкретный домен данных, а не использовать одну и ту же общую формулу для всех видов графиков.
- Отсутствие визуальной индикации текущей выбранной через клавиатуру точки графика (
m_selectedIndex) для зрячих пользователей, навигирующих с клавиатуры, но не использующих screen reader — реализацияpaintEvent()(не показанная полностью в примере) обязательно должна визуально подсвечивать выбранную точку (например, увеличенным маркером или вертикальной линией), синхронизируя визуальную и звуковую обратную связь для пользователей, которым нужна именно клавиатурная навигация без screen reader. - Чрезмерная сложность реализации полноценной доступности для каждого кастомного графического виджета в приложении с большим количеством разных типов визуализаций — для продуктов с обширной линейкой кастомных графиков создание подобной детальной поддержки доступности для каждого отдельного типа визуализации требует существенных трудозатрат; стоит приоритизировать усилия на действительно ключевых, часто используемых визуализациях, а не пытаться сразу обеспечить одинаково глубокую доступность для каждого мелкого графического элемента интерфейса.