Скринридер, которым пользуется незрячий человек, внешне выглядит как волшебство: программа вслух называет кнопки, поля ввода и пункты меню, озвучивает диалоги и даже описывает структуру сложных окон. На самом деле никакого распознавания пикселей здесь нет. Скринридер не "смотрит" на экран - он задаёт операционной системе вопросы через специальные программные интерфейсы, которые называются Accessibility API, то есть API доступности. Эта статья рассказывает, как устроен невидимый слой Windows, который делает интерфейс понятным не только глазам, но и программам, почему красивый самописный интерфейс может оказаться для незрячего пользователя пустым окном, и как та же самая идея деревьев доступности живёт в браузере и в автотестах.

Дерево доступности вместо пикселей

Главная идея всех Accessibility API проста: каждый элемент интерфейса - это не картинка, а объект с формальными свойствами. У кнопки есть роль "кнопка", имя "Сохранить" и состояние "доступна" или "отключена". У поля ввода есть роль "редактируемый текст", значение, позиция курсора и выделенный фрагмент. У флажка - роль "флажок" и состояние "установлен" или "снят". Всё множество таких объектов образует дерево доступности: окно содержит панель, панель содержит элементы, элементы содержат подэлементы. Стандартные элементы управления Windows - кнопки, списки, меню, вкладки - одновременно рисуют себя на экране и регистрируют себя в этом дереве, причём разработчику для этого обычно не нужно писать ни строчки дополнительного кода.

Скринридер работает как клиент этого дерева. Когда пользователь нажимает клавишу Tab и фокус переходит на следующий элемент, система посылает событие смены фокуса, скринридер запрашивает у элемента имя, роль и состояние и синтезирует фразу вроде "Сохранить, кнопка". Когда пользователь читает документ посимвольно или по словам, скринридер запрашивает текстовое содержимое и перемещается по нему программно. Ориентация по странице строится по структуре: можно прыгать по заголовкам, ссылкам, таблицам и ориентирам, а не листать экран сверху вниз. Для пользователя скринридера интерфейс - это навигация по дереву объектов, и качество этой навигации полностью определяется тем, насколько аккуратно дерево заполнено.

Важно понимать, что дерево доступности - структура данных, а не изображение. Элемент может существовать в дереве, но быть за пределами видимой области, свёрнутым или виртуализированным: строка в списке из миллиона записей существует в дереве логически, хотя нарисована только текущая страница. И наоборот: то, что нарисовано на экране, может отсутствовать в дереве вовсе - и тогда для скринридера его просто нет. Это двойственное свойство и делает технологию надёжной: она не зависит от разрешения, темы оформления и того, свёрнуто окно или развёрнуто, и вместе с тем требует дисциплины от автора интерфейса.

От MSAA до UI Automation

История этих API в Windows насчитывает почти три десятилетия. В 1997 году появилась Microsoft Active Accessibility, известная как MSAA, и её центральный интерфейс IAccessible. Модель была предельно простой: у каждого доступного объекта есть роль, имя, значение, состояние, местоположение на экране и ограниченный набор дочерних объектов и действий. Для приложений той эпохи этого хватало, и MSAA впервые сделала программный доступ к интерфейсу массовым явлением. Но простота была одновременно достоинством и ограничением: MSAA не умела описывать сложные элементы вроде сеток данных, древовидных представлений и форматированного редактируемого текста, масштабируемость была слабой, а событийная модель - грубой: программы вынуждены были допрашивать элементы повторно, чтобы заметить изменение.

Следующий шаг сделали в Windows Vista, где появился UI Automation - полноценная замена MSAA с гораздо более выразительной моделью. Вместо жёсткого списка свойств была введена концепция паттернов элементов управления. Паттерн - это интерфейс, который элемент поддерживает согласно своей роли. Кнопка поддерживает паттерн Invoke с одноимённым методом, поле ввода - паттерн Value с методами чтения и установки значения, текстовый редактор - паттерн Text с богатым API для навигации по символам, словам, строкам и абзацам, слайдер - паттерн RangeValue, таблица - паттерны Grid и Selection. Элемент может поддерживать сразу несколько паттернов, и скринридер опрашивает их, выясняя, что с элементом можно делать. Современные версии UI Automation развивают эту модель дальше, и все поддерживаемые фреймворки Windows - Win32, Windows Forms, WPF, WinUI - публикуют свои элементы в дерево UIA автоматически или с минимальными усилиями.

Отдельно стоит упомянуть вывод. Озвучивание строится на API синтеза речи SAPI: скринридер передаёт текст, движок синтеза превращает его в звук с выбранным голосом, темпом и интонацией. Опытные пользователи слушают речь на таком темпе, который зрячему человеку кажется неразборчивым, поэтому устойчивость синтеза и точная передача знаков препинания - не мелочь, а предмет ежедневной работы. Для брайлевских дисплеев - устройств с тактильной строкой из поднимающихся точек - существуют свои программные каналы: текст и положение курсора передаются на дисплей, пользователь читает пальцами и управляет фокусом клавишами самого устройства.

Почему красивый интерфейс бывает невидимым

У изящной архитектуры есть тёмная сторона: дерево доступности заполняется только тогда, когда элементы реально объявлены. Разработчик, который рисует интерфейс сам - по пикселям, на канве, собственными примитивами через графический контекст - получает полный художественный контроль, но по умолчанию публикует в дерево ровно одно пустое окно. Скринридер запрашивает у этого окна детей, получает пустой список и произносит что-нибудь утешительное вроде "неизвестное окно". Для зрячего пользователя внутри могут быть шикарные кнопки с анимацией; для незрячего - тишина.

Эта проблема объединяет несколько миров. Игры почти всегда рисуют интерфейс самостоятельно, а потому исторически были недоступны. Дизайнерские приложения и креативные инструменты любят собственные сценовые графы и холсты. Кроссплатформенные фреймворки, рендерящие интерфейс собственным движком, по умолчанию становятся непрозрачными для вспомогательных технологий. Решение одно и то же во всех случаях: построить виртуальное дерево доступности параллельно визуальному. Кастомный элемент должен реализовать провайдер Accessibility API: сообщить роль, имя, состояния, положение, поддержать нужные паттерны и генерировать события при изменениях. В современных фреймворках для этого есть формальные механизмы - достаточно сопоставить каждому нарисованному элементу логический узел и держать его актуальным. Это труд, но труд измеримый и хорошо задокументированный, и именно он отделяет "симпатичное окно" от профессионального продукта.

Соседние технологии доступности живут рядом и делят ту же философию. Экранная лупа использует свои API увеличения и отслеживания фокуса, фильтры цветокоррекции работают на системном уровне отрисовки, помогая людям с нарушениями цветовосприятия, а экранная клавиатура опирается на те же события фокуса и поля ввода из дерева доступности. Для слабовидящего пользователя связка лупы, контрастной темы и озвучивания трудных мест часто оказывается удобнее, чем любое одно средство в отдельности.

Дерево доступности в браузере

Та же самая архитектура работает и в вебе. Браузер анализирует разметку страницы и строит собственное дерево доступности - accessibility tree, которое отражает семантику элементов: ссылка, кнопка, список, заголовок, ориентир. Это дерево браузер выставляет наружу через тот самый системный UI Automation, и скринридер работает с вебом почти так же, как с обычным приложением Windows.

Отсюда вырастает смысл хорошо известных практик. Атрибут alt у изображения попадает в дерево доступности как имя объекта "изображение": без него скринридер произнесёт "изображение" или в лучшем случае имя файла, а с ним - осмысленное описание. Семантические роли, заголовки, подписи полей форм - всё это не украшения разметки, а прямое наполнение дерева. И наоборот, интерфейс, собранный из нейтральных контейнеров с обработчиками клика, рисуется красиво, но в дереве доступности представлен пустотой - ровно та же болезнь, что у самописного канва, только в браузерном варианте, отчего её иногда исправляют вспомогательными атрибутами, а ещё надёжнее - правильными элементами.

Сюда же относится и навигация с клавиатуры. Клавиша Tab двигает фокус по порядку фокусировки, и этот порядок должен совпадать с логикой страницы, а не с тем, как повезло в таблице стилей. Практика "скип-линков" - первой ссылки на странице, ведущей сразу к основному содержимому - избавляет пользователя от необходимости десять раз нажимать Tab сквозь навигационное меню на каждой странице. Для пользователя скринридера это экономия реального времени на каждом переходе. Симметричная ловушка - модальные диалоги: если фокус не перенесён внутрь диалога и не ограничен им, пользователь уходит клавишей Tab в фоновую страницу под подложкой и теряет контекст. Внимательные команды следят за тем, чтобы фокус после закрытия диалога возвращался ровно туда, откуда он был вызван.

Программы, которыми пользуются каждый день

В Windows встроен собственный скринридер - Экранный диктор, известный как Narrator. Он запускается по сочетанию клавиш и умеет читать интерфейс, документы и веб-страницы, поддерживает несколько режимов навигации и брайлевские дисплеи. В современных версиях Windows у него есть режим сканирования, в котором текст документа и страницы читается стрелками словно книга, а быстрые клавиши переносят к следующему заголовку, ссылке или таблице. Для быстрой проверки доступности приложения его достаточно: разработчик может надеть наушники, отвести взгляд от экрана и попробовать пройти свой рабочий сценарий вслепую.

В свободном мире стандартом фактически стал NVDA - свободный и открытый скринридер, написанный преимущественно на Python и невероятно расширяемый за счёт дополнений, которые сообщество публикует под любые специализированные приложения. NVDA активно использует UI Automation и клиентские API браузеров, поддерживает синтез речи и Брайль, а его бесплатность сделала доступность демократичной: качественный скринридер стал доступен каждому пользователю без лицензионного барьера. Существуют и коммерческие решения с многолетней историей, но NVDA и Экранный диктор сегодня покрывают основную массу потребностей и служат типичными целями тестирования.

Accessibility API и качество продукта

Самое неожиданное следствие этой архитектуры - влияние на инженерную культуру. Дерево доступности - машиночитаемое, а значит, им пользуется не только скринридер. Инструменты автоматизированного тестирования интерфейса ищут кнопки и поля по ролям и именам через те же самые Accessibility API: автотест "найди кнопку с именем Сохранить и вызови у неё Invoke" буквально использует то же рабочее русло, что и скринридер. Из этого следует ценная двойственность: команда, которая корректно публикует элементы в дерево доступности, бесплатно получает стабильный фундамент для автотестов, а команда, которая игнорирует доступность, платит дважды - теряет пользователей и вынуждена строить хрупкие тесты по координатам и пикселям, которые ломаются от любой смены темы или разрешения.

Практические шаги при этом хорошо известны и не требуют героизма:

  1. Использовать стандартные элементы управления там, где они решают задачу, а самописные - только сознательно и вместе с провайдером доступности.
  2. Проверять каждый экран клавиатурой: порядок Tab, видимость индикатора фокуса, отсутствие ловушек.
  3. Давать изображениям осмысленные alt-тексты, а полям форм - подписи.
  4. Проверять приложение Экранным диктором или NVDA как минимум на основных сценариях.
  5. Строить автотесты поверх ролей и имён из дерева доступности, а не поверх координат мыши.

Accessibility API - зрелая технология тридцатилетней выдержки, которая делает интерфейс структурой данных, а не картинкой. Команды, которые это понимают, пишут приложения, которыми можно пользоваться без мыши и без зрения, - и заодно получают чистую архитектуру и надёжные автотесты. Это редкий случай, когда инженерная дисциплина, забота о людях и выгода для проекта совпадают полностью.