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

Озвучивание событий интерфейса через QAccessibleEvent

Введение

В статьях 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(), перехватывающего и логирующего события для последующей проверки в автоматизированном тесте.