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

Навигация с клавиатуры: фокус, tab order и шорткаты

Введение

Многие пользователи — не только с ограниченными возможностями, но и те, кто предпочитает эффективную работу с клавиатуры, или временно не может использовать мышь (например, при использовании планшета без указателя) — должны иметь возможность выполнить любое действие в приложении исключительно через клавиатуру. В статье разберём три ключевых аспекта клавиатурной навигации в Qt: правильный порядок обхода фокуса (tab order), видимую индикацию фокуса, и горячие клавиши (shortcuts) для часто используемых действий.

Концепция

Tab order — последовательность, в которой фокус перемещается между виджетами при нажатии Tab/Shift+Tab — по умолчанию определяется порядком создания виджетов в коде, что не всегда совпадает с логичным, визуальным порядком расположения элементов на экране (особенно при сложных layout’ах). QWidget::setTabOrder() позволяет явно задать правильную последовательность. Шорткаты задаются через QShortcut или непосредственно через & в тексте действия меню (мнемоники) — критично, что они должны срабатывать независимо от того, какой конкретный виджет в данный момент находится в фокусе (в пределах разумной области действия — глобальные против локальных для конкретного виджета шорткатов).

Пример кода: Навигация с клавиатуры

#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QFormLayout>
#include <QShortcut>
#include <QKeySequence>

class LoginForm : public QWidget
{
    Q_OBJECT
public:
    explicit LoginForm(QWidget *parent = nullptr) : QWidget(parent)
    {
        auto *usernameField = new QLineEdit(this);
        auto *passwordField = new QLineEdit(this);
        passwordField->setEchoMode(QLineEdit::Password);

        auto *loginButton = new QPushButton("&Войти", this); // & задаёт мнемонику Alt+В
        auto *cancelButton = new QPushButton("&Отмена", this);

        auto *layout = new QFormLayout(this);
        layout->addRow("Имя пользователя:", usernameField);
        layout->addRow("Пароль:", passwordField);
        layout->addRow(loginButton, cancelButton);

        // Явный порядок обхода фокуса, гарантированно совпадающий
        // с визуальным порядком полей формы сверху вниз
        setTabOrder(usernameField, passwordField);
        setTabOrder(passwordField, loginButton);
        setTabOrder(loginButton, cancelButton);

        // Enter в любом текстовом поле формы активирует вход — частое
        // ожидание пользователей форм авторизации
        connect(usernameField, &QLineEdit::returnPressed, loginButton, &QPushButton::click);
        connect(passwordField, &QLineEdit::returnPressed, loginButton, &QPushButton::click);

        // Глобальный шорткат Escape для отмены — работает независимо от
        // того, какое конкретно поле находится в фокусе в момент нажатия
        auto *escapeShortcut = new QShortcut(QKeySequence(Qt::Key_Escape), this);
        connect(escapeShortcut, &QShortcut::activated, cancelButton, &QPushButton::click);

        usernameField->setFocus(); // фокус на первом логичном поле при открытии формы
    }
};

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

Символ & перед буквой в тексте кнопки ("&Войти") создаёт мнемонику — нажатие Alt+В (буква после &) активирует кнопку независимо от текущего фокуса, что особенно ценно в диалогах с несколькими кнопками, где нужен быстрый доступ без последовательного перебора Tab. setTabOrder() вызывается цепочкой, явно фиксируя порядок перехода фокуса между полями — без этого порядок определялся бы порядком создания объектов в коде, что в данном простом примере совпадает с желаемым, но в более сложных layout’ах (особенно при динамическом добавлении виджетов) может непредсказуемо отличаться.

Подключение returnPressed от обоих текстовых полей к loginButton->click() реализует стандартное пользовательское ожидание — нажатие Enter в любом поле формы логина должно отправить форму, а не просто перевести фокус на следующее поле (что было бы поведением Tab, а не Enter, в формах с одним полем на строку). QShortcut с Qt::Key_Escape, привязанный непосредственно к самой форме (this как родитель), работает в пределах области действия этой формы независимо от того, какой именно дочерний виджет внутри неё сейчас сфокусирован.

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

  • Tab order, не совпадающий с визуальным расположением элементов на экране. Пользователь, ориентирующийся на визуальный порядок (сверху вниз, слева направо), будет дезориентирован, если Tab перемещает фокус в порядке, не соответствующем этому ожиданию (например, из-за порядка создания виджетов в коде, отличного от их визуального расположения после применения сложного layout) — это особенно критично для пользователей screen reader, которые полностью полагаются на объявляемый порядок, не имея визуальной подсказки вообще.
  • Конфликтующие мнемоники в пределах одного видимого окна (например, два элемента с одинаковой буквой после &) приводят к тому, что соответствующий Alt+буква активирует не тот элемент, который ожидал пользователь, либо переключает между несколькими элементами с одинаковой мнемоникой по очереди при повторных нажатиях, что может сбивать с толку.
  • Шорткаты, перехватывающие стандартные системные комбинации клавиш (например, переопределение Ctrl+C для собственного действия там, где пользователь ожидает стандартного копирования текста) — нарушение устоявшихся платформенных конвенций клавиатурных сочетаний создаёт неожиданное и фрустрирующее поведение для опытных пользователей, привыкших к стандартному поведению этих комбинаций во всех приложениях.
  • Видимая индикация фокуса, отключённая или малозаметная из-за кастомного стиля оформления. Если приложение использует собственный, нестандартный визуальный стиль (QSS stylesheet), переопределяющий отображение фокуса, и забывает явно сохранить заметную, контрастную индикацию текущего сфокусированного элемента, пользователь, навигирующий только с клавиатуры (но без screen reader, то есть зрячий, но не использующий мышь), теряет возможность визуально понять, какой элемент сейчас активен — это легко проверить, просматривая интерфейс, перемещаясь только Tab без мыши.