Введение
Не все пользователи с нарушениями зрения используют screen reader — многие слабовидящие пользователи видят экран, но нуждаются в высоком контрасте между текстом и фоном, увеличенном размере текста, или отсутствии полагания исключительно на цвет для передачи важной информации (например, «красный — ошибка, зелёный — успех» без дополнительного текстового или иконографического обозначения недоступно для пользователей с дальтонизмом). В статье разберём практические приёмы обеспечения достаточной контрастности и независимости от цвета как единственного канала информации.
Концепция
Web Content Accessibility Guidelines (WCAG), хотя формально адресованы веб-контенту, дают универсально применимые количественные критерии и для десктопных приложений: коэффициент контрастности (соотношение яркости текста и фона) не менее 4.5:1 для обычного текста и 3:1 для крупного текста (уровень AA), и не менее 7:1/4.5:1 соответственно для более строгого уровня AAA. Расчёт коэффициента контрастности — стандартная, документированная формула на основе относительной яркости (relative luminance) цветов, которую можно реализовать программно для автоматической проверки палитры приложения, а не полагаться только на визуальную оценку дизайнером.
Пример кода: Контрастность интерфейса и
#include <QColor>
#include <QtMath>
// Реализация формулы относительной яркости и контрастности по WCAG
double relativeLuminance(const QColor &color)
{
auto linearize = [](double channel) {
channel /= 255.0;
return (channel <= 0.03928) ? channel / 12.92 : qPow((channel + 0.055) / 1.055, 2.4);
};
const double r = linearize(color.red());
const double g = linearize(color.green());
const double b = linearize(color.blue());
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
double contrastRatio(const QColor &foreground, const QColor &background)
{
const double l1 = relativeLuminance(foreground);
const double l2 = relativeLuminance(background);
const double lighter = qMax(l1, l2);
const double darker = qMin(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
bool meetsWcagAA(const QColor &foreground, const QColor &background, bool isLargeText = false)
{
const double ratio = contrastRatio(foreground, background);
return isLargeText ? (ratio >= 3.0) : (ratio >= 4.5);
}
// Автоматизированная проверка палитры приложения перед релизом (может быть частью теста)
#include <QtTest/QtTest>
class TestColorContrast : public QObject
{
Q_OBJECT
private slots:
void testPrimaryTextOnBackground()
{
const QColor textColor(51, 51, 51); // основной текст #333333
const QColor backgroundColor(255, 255, 255); // белый фон
QVERIFY2(meetsWcagAA(textColor, backgroundColor),
"Основной текст не соответствует WCAG AA по контрастности");
}
void testSecondaryTextOnAccentBackground()
{
// Часто проблемный случай: светлый текст на акцентном цвете кнопки
const QColor buttonTextColor(255, 255, 255);
const QColor buttonBackgroundColor(120, 170, 230); // недостаточно тёмный синий
QVERIFY2(meetsWcagAA(buttonTextColor, buttonBackgroundColor),
"Текст кнопки недостаточно контрастен на фоне акцентного цвета");
}
};
QTEST_APPLESS_MAIN(TestColorContrast)
#include "test_colorcontrast.moc"
// Пример избегания "цвет как единственный индикатор" в QML
import QtQuick
Row {
spacing: 8
// НЕДОСТАТОЧНО: только цвет различает статусы, неразличимо для дальтоника
// Rectangle { width: 12; height: 12; radius: 6; color: isOnline ? "green" : "red" }
// ЛУЧШЕ: цвет дополнен формой/иконкой/текстом
Rectangle {
width: 12; height: 12; radius: 6
color: isOnline ? "#4caf50" : "#f44336"
border.width: isOnline ? 0 : 2 // дополнительное визуальное отличие через форму
border.color: "white"
}
Text { text: isOnline ? "В сети" : "Не в сети" } // текстовое подтверждение статуса
property bool isOnline: true
}
Пояснения к коду
relativeLuminance() и contrastRatio() реализуют точную формулу WCAG для расчёта коэффициента контрастности между двумя цветами — нелинейное преобразование канала (linearize) учитывает нелинейность восприятия яркости человеческим глазом, и коэффициенты 0.2126/0.7152/0.0722 отражают разную чувствительность глаза к красному, зелёному и синему каналам соответственно (зелёный воспринимается значительно ярче при том же числовом значении канала).
TestColorContrast показывает, как превратить субъективную дизайнерскую оценку «выглядит достаточно контрастно» в автоматизированную, объективную проверку, интегрируемую в обычный набор тестов QTest — это позволяет ловить регрессии контрастности при изменении палитры приложения (например, при добавлении тёмной темы, разобранной в основном цикле статей про Qt Widgets) ещё на этапе CI, а не после жалоб пользователей. QML-пример демонстрирует принцип «не только цвет» — дублирование цветового индикатора статуса дополнительной текстовой подписью и отличием по форме (наличие/отсутствие белой обводки), что делает статус различимым даже для пользователя, не различающего красный и зелёный цвета.
Подводные камни
- Расчёт контрастности только для «основной» комбинации текст/фон без учёта состояний (hover, disabled, selected). Цвет текста на кнопке в обычном состоянии может быть достаточно контрастным, но при наведении (hover) или в неактивном состоянии (disabled) часто используются более светлые/приглушённые оттенки — именно эти, менее тщательно проверяемые состояния, часто оказываются недостаточно контрастными на практике.
- Полагание исключительно на автоматическую проверку коэффициента контрастности без визуальной проверки реального восприятия. Формула WCAG даёт объективную числовую метрику, но не заменяет реальное тестирование с пользователями с разными типами нарушений зрения или хотя бы симуляторами дальтонизма — два цвета могут формально соответствовать порогу контрастности, но всё равно создавать субъективно неприятное или сложное для восприятия сочетание.
- Жёстко закодированные цвета напрямую в QML/коде виджетов без централизованной системы токенов цвета. Если каждый компонент определяет свои цвета независимо, без единого источника правды (палитры, доступной для централизованной проверки и обновления), последовательная проверка контрастности всего приложения через автоматизированные тесты, как в примере, становится практически невозможной — нужна архитектура с централизованным набором именованных цветов, проверяемых один раз, а не разбросанных по всему коду.
- Игнорирование контрастности неактивного (disabled) состояния элементов под предлогом «это и не должно быть заметным». Хотя неактивные элементы по дизайну часто намеренно менее контрастны для визуального отличия от активных, это не означает, что они должны быть полностью нечитаемыми для слабовидящего пользователя — разумный компромисс между визуальной иерархией (активное/неактивное) и минимальной читаемостью должен быть найден явно, а не оставлен на волю случая при выборе оттенка серого «на глаз».