QML

Многостраничная навигация в QML: StackView, SwipeView, Drawer

Введение

Реальные приложения редко состоят из единственного экрана — в этой статье разберём навигационные паттерны Qt Quick: StackView для классической навигации «вперёд-назад» (как стек экранов мобильного приложения), SwipeView для горизонтальной навигации свайпом между страницами, и Drawer для выдвижной боковой панели навигации, привычной по многим современным мобильным и десктопным приложениям.

Концепция

StackView поддерживает стек страниц — push() добавляет новую страницу наверх стека (становящуюся видимой, с анимированным переходом, статья 487), а pop() возвращает к предыдущей странице, удаляя текущую — это естественно моделирует типичную навигацию «перейти глубже / вернуться назад», знакомую по большинству мобильных приложений. SwipeView располагает несколько страниц горизонтально и позволяет пользователю переключаться между ними свайпом (или программно через currentIndex), что отличается от StackView отсутствием понятия «глубины» — все страницы SwipeView равноправны и находятся на одном уровне. Drawer — панель, обычно скрытая за пределами видимой области экрана и выезжающая при определённом действии пользователя (свайп от края экрана, нажатие кнопки меню), типично используемая для основной навигации приложения по разным, не связанным напрямую разделам.

Пример кода: Многостраничная навигация в QML

// StackView — классическая навигация с push/pop
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    visible: true
    width: 400; height: 600

    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: mainPageComponent
    }

    Component {
        id: mainPageComponent
        Page {
            title: "Главная"
            Button {
                anchors.centerIn: parent
                text: "Открыть детали"
                onClicked: stackView.push(detailsPageComponent, {itemId: 42}) // передача параметра
            }
        }
    }

    Component {
        id: detailsPageComponent
        Page {
            property int itemId: -1
            title: "Детали элемента " + itemId

            Button {
                anchors.centerIn: parent
                text: "Назад"
                onClicked: stackView.pop() // возврат к предыдущей странице стека
            }
        }
    }
}
// SwipeView с индикатором текущей страницы (PageIndicator)
import QtQuick
import QtQuick.Controls

Column {
    anchors.fill: parent

    SwipeView {
        id: swipeView
        width: parent.width
        height: parent.height - 30

        Rectangle { color: "lightcoral"; Text { anchors.centerIn: parent; text: "Страница 1" } }
        Rectangle { color: "lightgreen"; Text { anchors.centerIn: parent; text: "Страница 2" } }
        Rectangle { color: "lightblue"; Text { anchors.centerIn: parent; text: "Страница 3" } }
    }

    PageIndicator {
        anchors.horizontalCenter: parent.horizontalCenter
        count: swipeView.count
        currentIndex: swipeView.currentIndex // привязка — индикатор автоматически отражает текущую страницу
    }
}
// Drawer — выдвижная боковая панель навигации
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    visible: true
    width: 400; height: 600

    Drawer {
        id: drawer
        width: 250; height: parent.height

        Column {
            ItemDelegate { text: "Главная"; onClicked: drawer.close() }
            ItemDelegate { text: "Настройки"; onClicked: drawer.close() }
            ItemDelegate { text: "О приложении"; onClicked: drawer.close() }
        }
    }

    ToolButton {
        text: "☰"
        onClicked: drawer.open()
    }
}

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

stackView.push(detailsPageComponent, {itemId: 42}) показывает передачу начальных значений свойств в новую страницу непосредственно при добавлении её в стек — detailsPageComponent получает itemId: 42 сразу при создании, что избегает необходимости отдельного, последующего вызова для передачи этого параметра. PageIndicator с привязкой currentIndex: swipeView.currentIndex показывает типичный паттерн синхронизации вспомогательного индикатора с реальным состоянием SwipeView через привязку (статья 481) — индикатор автоматически отражает, на какой странице сейчас находится пользователь, без какого-либо явного, дополнительного кода синхронизации. Drawer с drawer.close() внутри обработчиков ItemDelegate показывает типичный паттерн UX — выбор пункта меню сразу закрывает панель навигации, что ожидаемо для большинства пользователей, привыкших к такому поведению выдвижных навигационных панелей.

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

  • Накопление избыточно глубокого стека StackView без предусмотренного способа быстро вернуться к началу навигации — для приложений с потенциально глубокой навигацией (несколько последовательных push() подряд) стоит предусмотреть либо метод popToRoot() (или его эквивалент через pop(null), удаляющий весь стек до самой первой страницы), либо явную альтернативную навигацию, не вынуждающую пользователя многократно нажимать «назад» для возврата к началу.
  • Создание дорогих в инициализации страниц StackView заново при каждом push() без рассмотрения переиспользования уже созданных, но временно скрытых экземпляров — для страниц с дорогой инициализацией (загрузка данных, сложная разметка) стоит рассмотреть, действительно ли нужно создавать новый экземпляр при каждом переходе, или предпочтительнее переиспользовать существующий, временно скрытый экземпляр там, где логика приложения это допускает.
  • Игнорирование жестов платформы при использовании SwipeView на десктопе, где привычный для мобильных платформ свайп не имеет прямого эквивалента взаимодействия мышью — для десктопных приложений может понадобиться дополнительная, альтернативная навигация (кнопки «вперёд»/»назад», клавиатурные сочетания), не предполагающая обязательного использования жеста свайпа, неестественного для управления мышью.
  • Открытие Drawer без учёта модальности взаимодействия с остальным интерфейсом — по умолчанию Drawer может как блокировать взаимодействие с остальной частью приложения, пока он открыт (модальный режим), так и оставлять его доступным (немодальный режим), и выбор подходящего поведения (modal свойство) должен соответствовать продуманному, осознанному UX-решению, а не оставаться на поведении по умолчанию без явной проверки его соответствия ожиданиям конкретного приложения.