Введение
В статьях 200–201 мы уже касались QAccessible::updateAccessibility() для уведомления об изменении имени и значения элемента — в этой статье разберём более широкий набор событий доступности, доступных через систему QAccessibleEvent, и научимся правильно выбирать конкретный тип события для разных сценариев динамического изменения интерфейса: уведомления, валидация форм, изменение фокуса программным путём.
Концепция
QAccessibleEvent и его специализированные подклассы (QAccessibleValueChangeEvent, QAccessibleStateChangeEvent, QAccessibleTextInsertEvent и другие) описывают конкретный тип изменения, произошедшего с объектом доступности. Подсистема доступности платформы (screen reader через соответствующий платформенный API) подписывается на эти события и решает, как и когда сообщить о них пользователю — не каждое программное изменение должно сопровождаться явным голосовым объявлением (это могло бы быть слишком навязчивым потоком сообщений), и выбор правильного типа события и момента его отправки — важная часть проектирования действительно удобного для screen reader интерфейса.
Пример кода: Озвучивание событий интерфейса
#include <QAccessible>
#include <QLineEdit>
#include <QLabel>
class ValidatedFormField : public QWidget
{
Q_OBJECT
public:
explicit ValidatedFormField(const QString &fieldLabel, QWidget *parent = nullptr)
: QWidget(parent)
{
m_input = new QLineEdit(this);
m_errorLabel = new QLabel(this);
m_errorLabel->setStyleSheet("color: red;");
m_errorLabel->hide();
m_input->setAccessibleName(fieldLabel);
auto *layout = new QVBoxLayout(this);
layout->addWidget(m_input);
layout->addWidget(m_errorLabel);
connect(m_input, &QLineEdit::textChanged, this, &ValidatedFormField::validate);
}
private slots:
void validate()
{
const bool isValid = !m_input->text().trimmed().isEmpty();
if (isValid == m_wasValid) {
return; // состояние не изменилось — избегаем избыточных уведомлений
}
m_wasValid = isValid;
if (!isValid) {
m_errorLabel->setText("Это поле обязательно для заполнения");
m_errorLabel->show();
// Объявляем ошибку валидации — критично важное сообщение,
// которое должно быть озвучено немедленно, не дожидаясь
// перевода фокуса пользователем на другой элемент
QAccessibleEvent errorEvent(m_errorLabel, QAccessible::Alert);
QAccessible::updateAccessibility(&errorEvent);
// Дополнительно помечаем само поле ввода как имеющее невалидное состояние
QAccessible::State invalidState;
invalidState.invalid = true;
QAccessibleStateChangeEvent stateEvent(m_input, invalidState);
QAccessible::updateAccessibility(&stateEvent);
} else {
m_errorLabel->hide();
QAccessible::State validState;
validState.invalid = false;
QAccessibleStateChangeEvent stateEvent(m_input, validState);
QAccessible::updateAccessibility(&stateEvent);
}
}
private:
QLineEdit *m_input;
QLabel *m_errorLabel;
bool m_wasValid = true;
};
Пояснения к коду
Проверка if (isValid == m_wasValid) return; предотвращает избыточные, повторяющиеся уведомления подсистемы доступности при каждом нажатии клавиши, если состояние валидности фактически не изменилось — без этой проверки screen reader мог бы повторно объявлять «поле обязательно для заполнения» при каждом нажатии клавиши в уже невалидном поле, что было бы крайне навязчивым и бесполезным потоком сообщений.
QAccessible::Alert — тип события, специально предназначенный для важных, требующих немедленного внимания сообщений (в отличие от обычного NameChanged/ValueChanged, которые могут быть озвучены менее настойчиво или только при следующем перемещении фокуса) — это подходящий выбор для сообщений об ошибках валидации, которые пользователь должен узнать сразу, не дожидаясь, пока он сам обратит внимание на изменившийся визуальный текст. QAccessibleStateChangeEvent с флагом invalid дополнительно сообщает screen reader, что само поле находится в состоянии ошибки — многие screen reader используют именно это состояние для специального, отличающегося звукового сигнала при попадании фокуса на невалидное поле, независимо от текста объявления ошибки.
Подводные камни
- Избыточное использование
QAccessible::Alertдля несущественных, частых изменений. Если каждое мелкое, не критичное изменение интерфейса оформляется какAlert, пользователь screen reader получает постоянный поток прерывающих, «громких» объявлений, что хуже, чем их полное отсутствие — этот тип события нужно резервировать действительно для важных, требующих немедленного внимания событий (ошибки, критичные предупреждения), а не использовать как общий механизм уведомления о любом изменении. - Отсутствие уведомления о программном изменении фокуса, инициированном кодом приложения, а не пользователем. Если приложение программно переводит фокус на другой виджет (например, после завершения некой операции) без соответствующего события
QAccessible::Focus, screen reader может не заметить это изменение и продолжать сообщать о ранее сфокусированном элементе — для таких программных изменений фокуса нужно либо использовать стандартные методы Qt, которые сами корректно генерируют необходимые события (какQWidget::setFocus()в большинстве случаев), либо проверять, действительно ли событие доступности было сгенерировано автоматически. - Race condition между быстрым последовательным изменением состояния и обработкой события screen reader’ом. Если состояние меняется быстрее, чем screen reader успевает обработать и озвучить предыдущее событие (например, при очень быстром вводе текста, вызывающем частую валидацию), часть промежуточных уведомлений может быть пропущена самим screen reader, и пользователь услышит только финальное состояние — для большинства практических случаев это приемлемо (важен конечный результат, а не каждое промежуточное состояние), но стоит иметь в виду при проектировании логики уведомлений для очень быстро изменяющихся данных.
- Сложность тестирования корректности отправки и интерпретации событий доступности без реального screen reader. В отличие от обычной функциональной логики, события
QAccessibleEventсами по себе не имеют видимого эффекта в обычном автоматизированном UI-тесте (QTest не проверяет, что именно «услышал» бы screen reader) — для проверки того, что события действительно отправляются с корректными параметрами в нужные моменты, обычно нужны либо специализированные инструменты тестирования доступности, либо явная регистрация собственного «тестового»QAccessible::installFactory(), перехватывающего и логирующего события для последующей проверки в автоматизированном тесте.