Evgenii Legotckoi
Evgenii LegotckoiҚар. 28, 2015, 10:06 Т.Ж.

QML - 019-сабақ. Qt Qml Android жүйесіндегі навигация тартпасы

Одним из основных элементов управления в Android приложении является Navigation Drawer , но в QML нет готового компонента для этого элемента, зато парни из Cutehacks сделали свой Navigation Drawer , код этого компонента выложен на гитхабе. Я давно уже хотел запустить этот код на живом Android устройстве и вот руки наконец-то до него дотянулись.

Я подробно изучил этот код и немного его подправил, поскольку в той версии было некоторое несоответствие Material Design в том плане, что панелька раскрывалась на 78 процентов от ширины экрана вне зависимости от ориентации. А Material Design рекомендует в портретной ориентации раскрывать Navigation Drawer так, чтобы он не доходил до противоположного края на 56 dip в случае со смартфонами и на 64 dip в случае с планшетами, но сделаем хотя бы для смартфонов, а в ландшафтной ориентации был не более, чем 320 dip шириной. Что я и поправил, также выпилив малую часть часть ненужного на данный момент кода и немного переименовав переменные под себя.

Что касается величины dip , то есть пикселей независимых от плотности экрана устройства, то это уже вопрос правильного масштабирования элементов интерфейса .

Предлагаю Вашему вниманию пример использования данного Navigation Drawer для смены трёх фрагментов в объект Loader с помощью трёх пунктов меню, которые будут находиться в данном Navigation Drawer .

Структура проекта для работы с Navigation Drawer

Структура проект будет сходна со структурой проекта из статьи по изучению работы с компонентом Loader .

  • QmlNavigationDrawer.pro - профайл проекта;
  • main.cpp - основной файл исходных кодов приложения;
  • main.qml - основной файл кодов qml;
  • Fragment1.qml - первый фрагмент для замены в Loader;
  • Fragment2.qml - второй фрагмент;
  • Fragment3.qml - третий фрагмент.
  • NavigationDrawer.qml - сам объект Navigation Drawer.

main.cpp создаётся по умолчанию и не подвергается изменениям, поэтому его листинг приводится не будет, также как и профайла проекта.

main.qml

В нашем приложении будет Application Bar , который является объектом типа Rectangle, и в него Мы поместим иконку-гамбургер, по нажатию на которую будем открывать и закрывать Navigation Drawer посредством функции toggle() , при этом он будет перекрывать Application Bar в соответствии с рекомендациями Material Design . А ниже Application Bar всё оставшееся пространство будет занимать компонент Loader, в котором мы и будем менять фрагменты.

Также поместим в коде объект Navigation Drawer , в который поместим ListView. В данном ListView будет располагаться список пунктов меню. По нажатию пункт меню будет вызывать функцию смены компонента в Loader, передавая в данную функцию свой индекс. И уже по индекс Loader определит, какой компонент необходимо загрузить.

import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Window 2.0
import QtQuick.Layouts 1.1

ApplicationWindow {
    visible: true
    width: 700
    height: 480
    title: qsTr("Hello World")

    // Пересчёт независимых от плотности пикселей в физические пиксели устройства
    readonly property int dpi: Screen.pixelDensity * 25.4
    function dp(x){ return (dpi < 120) ? x : x*(dpi/160); }

    // Application Bar
    Rectangle {
        id: menuRect
        anchors.top: parent.top
        anchors.left: parent.left
        anchors.right: parent.right
        height: dp(48)
        color: "#4cd964"

        // Иконка-Гамбургер
        Rectangle {
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            anchors.left: parent.left

            width: dp(48)
            color: "#4cd964"

            Rectangle {
                anchors.top: parent.top
                anchors.topMargin: dp(16)
                anchors.left: parent.left
                anchors.leftMargin: dp(14)
                width: dp(20)
                height: dp(2)
            }

            Rectangle {
                anchors.top: parent.top
                anchors.topMargin: dp(23)
                anchors.left: parent.left
                anchors.leftMargin: dp(14)
                width: dp(20)
                height: dp(2)
            }

            Rectangle {
                anchors.top: parent.top
                anchors.topMargin: dp(30)
                anchors.left: parent.left
                anchors.leftMargin: dp(14)
                width: dp(20)
                height: dp(2)
            }

            MouseArea {
                anchors.fill: parent

                onClicked: {
                    nav.toggle()
                }
            }
        }

    }

    // Loader для смены Фрагментов
    Loader {
        id: loader
        anchors.top: menuRect.bottom
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        source: "Fragment1.qml"

        // Функция для смены содержимого Loader
        function loadFragment(index){

            switch(index){
            case 0:
                loader.source = "Fragment1.qml"
                break;
            case 1:
                loader.source = "Fragment2.qml"
                break;
            case 2:
                loader.source = "Fragment3.qml"
                break;
            default:
                loader.source = "Fragment1.qml"
                break;
            }
        }
    }

    NavigationDrawer {
        id: nav
        Rectangle {
            anchors.fill: parent
            color: "white"

            // Список с пунктами меню
            ListView {
                anchors.fill: parent

                delegate: Item {
                    height: dp(48)
                    anchors.left: parent.left
                    anchors.right: parent.right

                    Rectangle {
                        anchors.fill: parent
                        anchors.margins: dp(5)
                        color: "whitesmoke"

                        Text {
                            text: fragment
                            anchors.fill: parent
                            font.pixelSize: dp(20)

                            renderType: Text.NativeRendering
                            horizontalAlignment: Text.AlignHCenter
                            verticalAlignment: Text.AlignVCenter
                        }

                        MouseArea {
                            anchors.fill: parent

                            // По нажатию на пункт меню заменяем компонент в Loader
                            onClicked: {
                                loader.loadFragment(index)
                            }
                        }
                    }
                }

                model: navModel
            }
        }
    }

    // Модель данных для списка с пунктами меню
    ListModel {
        id: navModel

        ListElement {fragment: "Fragment 1"}
        ListElement {fragment: "Fragment 2"}
        ListElement {fragment: "Fragment 3"}
    }
}

Fragment1.qml и остальные фрагменты

Код Fragment1.qml и остальных фрагментов идентичен за исключением того, что у них у всех разный цвет фона.

import QtQuick 2.5

Rectangle {
    anchors.fill: parent
    color: "green"

    Text {
        text: "Fragment 1"
        color: "white"
        anchors.top: parent.top
        anchors.right: parent.right
        anchors.margins: dp(50)
        font.pixelSize: dp(30)

        renderType: Text.NativeRendering
    }

}

NavigationDrawer.qml

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

import QtQuick 2.5
import QtQuick.Window 2.0

Rectangle {
    id: panel

    // Пересчёт независимых от плотности пикселей в физические пиксели устройства
    readonly property int dpi: Screen.pixelDensity * 25.4
    function dp(x){ return (dpi < 120) ? x : x*(dpi/160); }

    // Свойства Navigation Drawer
    property bool open: false           // Состояние Navigation Drawer - Открыт/Закрыт
    property int position: Qt.LeftEdge  // Положение Navigation Drawer - Слева/Справа

    // Функции открытия и закрытия Navigation Drawer
    function show() { open = true; }
    function hide() { open = false; }
    function toggle() { open = open ? false : true; }

    // Внутренние свойства Navigation Drawer
    readonly property bool _rightEdge: position === Qt.RightEdge
    readonly property int _closeX: _rightEdge ? _rootItem.width : - panel.width
    readonly property int _openX: _rightEdge ? _rootItem.width - width : 0
    readonly property int _minimumX: _rightEdge ? _rootItem.width - panel.width : -panel.width
    readonly property int _maximumX: _rightEdge ? _rootItem.width : 0
    readonly property int _pullThreshold: panel.width/2
    readonly property int _slideDuration: 260
    readonly property int _openMarginSize: dp(20)

    property real _velocity: 0
    property real _oldMouseX: -1

    property Item _rootItem: parent

    on_RightEdgeChanged: _setupAnchors()
    onOpenChanged: completeSlideDirection()

    width: (Screen.width > Screen.height) ? dp(320) : Screen.width - dp(56)
    height: parent.height
    x: _closeX
    z: 10

    function _setupAnchors() {
        _rootItem = parent;

        shadow.anchors.right = undefined;
        shadow.anchors.left = undefined;

        mouse.anchors.left = undefined;
        mouse.anchors.right = undefined;

        if (_rightEdge) {
            mouse.anchors.right = mouse.parent.right;
            shadow.anchors.right = panel.left;
        } else {
            mouse.anchors.left = mouse.parent.left;
            shadow.anchors.left = panel.right;
        }

        slideAnimation.enabled = false;
        panel.x = _rightEdge ? _rootItem.width :  - panel.width;
        slideAnimation.enabled = true;
    }

    function completeSlideDirection() {
        if (open) {
            panel.x = _openX;
        } else {
            panel.x = _closeX;
            Qt.inputMethod.hide();
        }
    }

    function handleRelease() {
        var velocityThreshold = dp(5)
        if ((_rightEdge && _velocity > velocityThreshold) ||
                (!_rightEdge && _velocity < -velocityThreshold)) {
            panel.open = false;
            completeSlideDirection()
        } else if ((_rightEdge && _velocity < -velocityThreshold) ||
                   (!_rightEdge && _velocity > velocityThreshold)) {
            panel.open = true;
            completeSlideDirection()
        } else if ((_rightEdge && panel.x < _openX + _pullThreshold) ||
                   (!_rightEdge && panel.x > _openX - _pullThreshold) ) {
            panel.open = true;
            panel.x = _openX;
        } else {
            panel.open = false;
            panel.x = _closeX;
        }
    }

    function handleClick(mouse) {
        if ((_rightEdge && mouse.x < panel.x ) || mouse.x > panel.width) {
            open = false;
        }
    }

    onPositionChanged: {
        if (!(position === Qt.RightEdge || position === Qt.LeftEdge )) {
            console.warn("SlidePanel: Unsupported position.")
        }
    }

    Behavior on x {
        id: slideAnimation
        enabled: !mouse.drag.active
        NumberAnimation {
            duration: _slideDuration
            easing.type: Easing.OutCubic
        }
    }

    NumberAnimation on x {
        id: holdAnimation
        to: _closeX + (_openMarginSize * (_rightEdge ? -1 : 1))
        running : false
        easing.type: Easing.OutCubic
        duration: 200
    }

    MouseArea {
        id: mouse
        parent: _rootItem

        y: _rootItem.y
        width: open ? _rootItem.width : _openMarginSize
        height: _rootItem.height
        onPressed:  if (!open) holdAnimation.restart();
        onClicked: handleClick(mouse)
        drag.target: panel
        drag.minimumX: _minimumX
        drag.maximumX: _maximumX
        drag.axis: Qt.Horizontal
        drag.onActiveChanged: if (active) holdAnimation.stop()
        onReleased: handleRelease()
        z: open ? 1 : 0
        onMouseXChanged: {
            _velocity = (mouse.x - _oldMouseX);
            _oldMouseX = mouse.x;
        }
    }

    Connections {
        target: _rootItem
        onWidthChanged: {
            slideAnimation.enabled = false
            panel.completeSlideDirection()
            slideAnimation.enabled = true
        }
    }

    Rectangle {
        id: backgroundBlackout
        parent: _rootItem
        anchors.fill: parent
        opacity: 0.5 * Math.min(1, Math.abs(panel.x - _closeX) / _rootItem.width/2)
        color: "black"
    }

    Item {
        id: shadow
        anchors.left: panel.right
        anchors.leftMargin: _rightEdge ? 0 : dp(10)
        height: parent.height

        Rectangle {
            height: dp(10)
            width: panel.height
            rotation: 90
            opacity: Math.min(1, Math.abs(panel.x - _closeX)/ _openMarginSize)
            transformOrigin: Item.TopLeft
            gradient: Gradient{
                GradientStop { position: _rightEdge ? 1 : 0 ; color: "#00000000"}
                GradientStop { position: _rightEdge ? 0 : 1 ; color: "#2c000000"}
            }
        }
    }
}

Итог

В результате написанное приложение будет выглядеть так, как показано на ниже следующих рисунках. Также в видеоуроке показана демонстрация приложения как на Desktop , так и на Android устройстве.

Ссылка на скачивание проекта в zip-архиве: QML Navigation Drawer

Видеоурок

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Ол саған ұнайды ма? Әлеуметтік желілерде бөлісіңіз!

Константин Козлов
  • Ақп. 17, 2018, 2:50 Т.Ж.

Не работает, если запихать Navigation Drawer в элемент List View. Drawer срабатывает только для первого элемента в списке.

Константин Козлов
  • Ақп. 17, 2018, 8:11 Т.Ж.

Уже разобрался, где ошибка. В 126-й строке должно быть не

y: _rootItem.y
, а
y: 0
Кроме того, QML ругается на 135-ю строку. Думаю, там должно быть
drag.onActiveChanged: if (drag.active) holdAnimation.stop()
Evgenii Legotckoi
  • Ақп. 18, 2018, 7:23 Т.Ж.

Добрый день!

Вполне возможно, что есть некоторые несостыковки из-за версий компонентов QML.
А вообще, попробуйте Navigation Drawer из последних компонентов Qt. Дело в том, что этот урок несколько устарел, а Qt Company выпустила свои собственные компоненты, в составе которых есть и Navigation Drawer. Вы можете ознакомиться с этими компонентами в примере Qt Gallery, который присутствует в составе примеров в Qt Creator. Думаю, что это будет для ваших задач и целей лучше.
Константин Козлов
  • Ақп. 18, 2018, 8:28 Т.Ж.

Я считаю, что это всё же ошибка, т. к. координаты Item'а всегда задаются относительно родителя. Соответственно, чтобы MouseArea занимала по высоте весь родительский элемент, её координата Y должна быть равна 0, а не координате Y родительского элемента.


А по поводу Drawer'а, появившегося в Qt 5.6, я не хочу его использовать, т. к. желаю сохранить совместимость с Qt 5.5.

Ясно, вопросы обратной совместимости.

Да, проверил, как-то криво работает, если не поправить 126-ю строку, в том случае, если Navigation Drawer требуется поместить в какой-то внутренний объект, хотя для меня сам по себе User Case странный. Получается, что вы допускаете случай, в котором Drawer может быть развёрнут лишь на часть окна приложения, иначе я не вижу смысла помещать его в какое-либо иное место, кроме самого окна приложения.
Константин Козлов
  • Ақп. 18, 2018, 9:29 Т.Ж.

Это как раз мой Use Case, потому-то я и задал тут вопрос.
Дело в том, что я использую Ваш Dawer не только для главного меню приложения, но и для контекстного меню элементов ListView.

Evgenii Legotckoi
  • Ақп. 18, 2018, 9:42 Т.Ж.

Да, теперь представляю, как то работает. Согласен, ваша правка определённо к месту здесь.

Пікірлер

Тек рұқсаты бар пайдаланушылар ғана пікір қалдыра алады.
Кіріңіз немесе Тіркеліңіз
Г

C++ - Тест 001. Первая программа и типы данных

  • Нәтиже:66ұпай,
  • Бағалау ұпайлары-1
t

C++ - Тест 001. Первая программа и типы данных

  • Нәтиже:33ұпай,
  • Бағалау ұпайлары-10
t

Qt - Тест 001. Сигналы и слоты

  • Нәтиже:52ұпай,
  • Бағалау ұпайлары-4
Соңғы пікірлер
G
GoattRockҚыр. 3, 2024, 1:50 Т.Қ.
Linux жүйесінде файлдарды қалай көшіруге болады Задумывались когда-нибудь о том, как мы привыкли доверять свои вещи службам грузоперевозок? Сейчас такие услуги стали неотъемлемой частью нашей жизни, особенно когда речь идет о переездах между …
d
dblas5Шілде 5, 2024, 11:02 Т.Ж.
QML - Сабақ 016. SQLite деректер қоры және онымен QML Qt-та жұмыс істеу Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssrАқп. 8, 2024, 6:43 Т.Қ.
Qt Linux - Сабақ 001. Linux астында Autorun Qt қолданбасы как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий КононенкоАқп. 5, 2024, 1:50 Т.Ж.
Qt WinAPI - Сабақ 007. Qt ішінде ICMP Ping арқылы жұмыс істеу Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
Енді форумда талқылаңыз
Evgenii Legotckoi
Evgenii LegotckoiМаусым 24, 2024, 3:11 Т.Қ.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
F
FynjyШілде 22, 2024, 4:15 Т.Ж.
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …
BlinCT
BlinCTМаусым 25, 2024, 1 Т.Ж.
Нарисовать кривую в qml Всем привет. Имеется Лист листов с тосками, точки получаны интерполяцией Лагранжа. Вопрос, как этими точками нарисовать кривую? ChartView отпадает сразу, в qt6.7 появился новый элемент…
BlinCT
BlinCTМамыр 5, 2024, 5:46 Т.Ж.
Написать свой GraphsView Всем привет. В Qt есть давольно старый обьект дял работы с графиками ChartsView и есть в 6.7 новый но очень сырой и со слабым функционалом GraphsView. По этой причине я хочу написать х…
Evgenii Legotckoi
Evgenii LegotckoiМамыр 2, 2024, 2:07 Т.Қ.
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Добрый день. По моему мнению - да, но то, что будет касаться вызовов к функционалу Андроида, может создать огромные трудности.

Бізді әлеуметтік желілерде бақылаңыз