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

Доступность в QML/Qt Quick: Accessible attached property

Введение

В Qt Quick доступность реализуется иначе, чем в Qt Widgets — вместо отдельного класса QAccessibleInterface, регистрируемого через фабрику, для QML-элементов используется attached property Accessible, которая декларативно прикрепляется к любому визуальному элементу прямо в QML-коде. В статье разберём, как настроить доступность для типичных QML-компонентов — кастомной кнопки и переключателя, не использующих стандартные Qt Quick Controls.

Концепция

Attached property Accessible доступна для любого Item в QML после установки Accessible.role — само это присвоение активирует экспорт доступности для элемента (по умолчанию произвольный Item не считается значимым для доступности и пропускается подсистемой). Основные свойства: Accessible.role (семантическая роль — Button, CheckBox, StaticText и другие), Accessible.name (озвучиваемое имя), Accessible.description (дополнительный контекст), и для интерактивных элементов — Accessible.onPressAction/другие обработчики, позволяющие screen reader программно активировать элемент, даже если он не предоставляет реальной точки клика для эмулируемого нажатия.

Пример кода: Доступность в QML/Qt Quick

// CustomButton.qml — кастомная кнопка без использования Qt Quick Controls
import QtQuick

Rectangle {
    id: root
    property string text: ""
    signal clicked()

    width: 120; height: 40
    radius: 6
    color: mouseArea.pressed ? "#3a78c2" : "#4a90e2"

    Accessible.role: Accessible.Button
    Accessible.name: root.text
    Accessible.onPressAction: root.clicked() // позволяет screen reader "нажать" кнопку программно

    Text {
        anchors.centerIn: parent
        text: root.text
        color: "white"
    }

    MouseArea {
        id: mouseArea
        anchors.fill: parent
        onClicked: root.clicked()
    }
}
// CustomToggle.qml — кастомный переключатель с корректным сообщением состояния
import QtQuick

Rectangle {
    id: root
    property bool checked: false
    signal toggled(bool checked)

    width: 50; height: 26
    radius: height / 2
    color: checked ? "#4caf50" : "#cccccc"

    Accessible.role: Accessible.CheckBox
    Accessible.name: "Включить уведомления"
    // checkable/checked сообщают screen reader текущее состояние переключателя —
    // без этого он не сможет озвучить "включено"/"выключено"
    Accessible.checkable: true
    Accessible.checked: root.checked
    Accessible.onToggleAction: {
        root.checked = !root.checked;
        root.toggled(root.checked);
    }

    Rectangle {
        width: 20; height: 20; radius: 10
        color: "white"
        x: root.checked ? root.width - width - 3 : 3
        anchors.verticalCenter: parent.verticalCenter

        Behavior on x { NumberAnimation { duration: 150 } }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: {
            root.checked = !root.checked;
            root.toggled(root.checked);
        }
    }
}

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

CustomButton устанавливает Accessible.role: Accessible.Button и Accessible.name, привязанное к видимому тексту кнопки — этого достаточно для базового объявления роли и имени элемента. Критичная деталь — Accessible.onPressAction, привязанный к тому же сигналу clicked(), что и обычный обработчик MouseArea — это даёт screen reader возможность программно «нажать» кнопку (например, через комбинацию клавиш активации текущего элемента в самом screen reader) без необходимости физически кликать мышью, что для незрячего пользователя, ориентирующегося полностью на клавиатуру и озвучивание, абсолютно необходимо.

CustomToggle дополнительно использует Accessible.checkable/Accessible.checked для сообщения булевого состояния переключателя, и Accessible.onToggleAction — специализированный обработчик именно для переключаемых элементов, логически отличный от onPressAction обычной кнопки, поскольку переключатель имеет двойное состояние, которое должно быть программно изменяемым через ту же активацию из screen reader, а не только через визуальный клик мышью.

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

  • Отсутствие Accessible.role делает элемент полностью невидимым для подсистемы доступности. Простое присвоение Accessible.name без указания role в некоторых конфигурациях может не активировать экспорт доступности для элемента вовсе — оба свойства обычно нужно устанавливать совместно для надёжной работы, а не полагаться на то, что одно из них автоматически подразумевает другое.
  • Кастомный визуальный компонент без соответствующего Accessible.onPressAction/onToggleAction виден screen reader (если роль и имя установлены), но не интерактивен через программную активацию — пользователь screen reader услышит название элемента, но не сможет с ним взаимодействовать без реального физического клика мышью, что для многих пользователей screen reader полностью недостижимо.
  • Несоответствие визуального состояния и объявленного Accessible.checked при сложной логике переключения. Если состояние checked элемента переключается асинхронно (например, после подтверждения с сервера, а не мгновенно при клике), и привязка Accessible.checked: root.checked ссылается на локальное, оптимистично изменённое значение до получения реального подтверждения, screen reader может на короткое время объявить состояние, не соответствующее тому, что фактически подтверждено на сервере — для критичных переключателей стоит явно синхронизировать визуальное/декларируемое состояние именно с подтверждённым, а не оптимистично предсказанным значением.
  • Использование стандартных Qt Quick Controls (Button, CheckBox из QtQuick.Controls) вместо полностью кастомных компонентов там, где доступность уже встроена «из коробки». Стандартные контролы Qt Quick Controls уже имеют базовую интеграцию с подсистемой доступности без необходимости вручную прописывать каждое свойство Accessible.* — переход на полностью кастомные визуальные компоненты (как в примере) оправдан только когда есть реальная потребность в нестандартном внешнем виде, и в этом случае вся ответственность за корректную доступность переходит на разработчика компонента, что и продемонстрировано необходимым объёмом кода в примерах этой статьи.