Evgenii Legotckoi
Evgenii Legotckoi28 листопада 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 р. 02:50

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

Константин Козлов
  • 17 лютого 2018 р. 08:11

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

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

Добрый день!

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

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


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

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

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

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

Evgenii Legotckoi
  • 18 лютого 2018 р. 09:42

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

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
AD

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:50бали,
  • Рейтинг балів-4
m
  • molni99
  • 26 жовтня 2024 р. 01:37

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80бали,
  • Рейтинг балів4
m
  • molni99
  • 26 жовтня 2024 р. 01:29

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:20бали,
  • Рейтинг балів-10
Останні коментарі
ИМ
Игорь Максимов22 листопада 2024 р. 11:51
Django - Підручник 017. Налаштуйте сторінку входу до Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 жовтня 2024 р. 14:37
Django - Урок 064. Як написати розширення для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 жовтня 2024 р. 08:19
Читалка файлів fb3 на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов05 жовтня 2024 р. 07:51
Django - Урок 064. Як написати розширення для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas505 липня 2024 р. 11:02
QML - Урок 016. База даних SQLite та робота з нею в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Тепер обговоріть на форумі
Evgenii Legotckoi
Evgenii Legotckoi24 червня 2024 р. 15:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 листопада 2024 р. 06:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject04 червня 2022 р. 03:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9Anonim25 жовтня 2024 р. 09:10
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Слідкуйте за нами в соціальних мережах