Делегаты в QML

Добавлено 11 апреля 2022 в 20:48

Когда дело доходит до использования моделей и представлений в пользовательском интерфейсе, делегат играет огромную роль в создании внешнего вида и поведения. Поскольку каждый элемент модели визуализируется через делегата, то, что на самом деле видно пользователю, – это делегаты.

Каждый делегат получает доступ к ряду прикрепленных свойств, некоторые из модели данных, другие из представления. Из модели свойства передают делегату данные для каждого элемента. Из представления свойства передают информацию о состоянии, связанную с делегатом в представлении. Давайте пройдемся по свойствам из представления.

Наиболее часто используемые свойства, прикрепленные из представления, – это ListView.isCurrentItem и ListView.view. Первое – это логическое значение, указывающее, является ли элемент текущим элементом, а второе – доступная только для чтения ссылка на фактическое представление. Благодаря доступу к представлению можно создавать общие повторно используемые делегаты, которые адаптируются к размеру и характеру представления, в котором они содержатся. В приведенном ниже примере ширина каждого делегата привязана к ширине представления, а цвет фона каждого делегата зависит от прикрепленного свойства ListView.isCurrentItem.

import QtQuick

Rectangle {
    width: 120
    height: 300

    gradient: Gradient {
        GradientStop { position: 0.0; color: "#f6f6f6" }
        GradientStop { position: 1.0; color: "#d7d7d7" }
    }

    ListView {
        anchors.fill: parent
        anchors.margins: 20

        focus: true

        model: 100
        delegate: numberDelegate

        spacing: 5
        clip: true
    }

    Component {
        id: numberDelegate

        Rectangle {
            id: wrapper

            required property int index

            width: ListView.view.width
            height: 40

            color: ListView.isCurrentItem ? "#157efb" : "#53d769"
            border.color: Qt.lighter(color, 1.1)

            Text {
                anchors.centerIn: parent

                font.pixelSize: 10

                text: wrapper.index
            }
        }
    }
}
использование свойств ListView в делегате

Если каждый элемент в модели связан с действием, например, клик по элементу влияет на него, эта функциональность является частью каждого делегата. Это разделяет управление событиями между представлением, которое обрабатывает навигацию между элементами в представлении, и делегатом, который обрабатывает действия над конкретным элементом.

Самый простой способ сделать это – создать MouseArea внутри каждого делегата и реагировать на сигнал onClicked. Это продемонстрировано на примере в следующем разделе данной главы.

Анимация добавления и удаления элементов

В некоторых случаях содержимое, отображаемое в представлении, со временем меняется. Элементы добавляются и удаляются по мере изменения базовой модели данных. В этих случаях часто полезно использовать визуальные подсказки, чтобы дать пользователю ощущение направления и помочь ему понять, какие данные добавляются или удаляются.

Достаточно удобно, что представления QML присоединяют к каждому делегату элемента два сигнала, onAdd и onRemove. Запуская из них анимацию, легко создать движение, необходимое для того, чтобы помочь пользователю определить, что происходит.

Пример ниже демонстрирует это с помощью динамически заполняемой модели ListModel. В нижней части экрана показана кнопка для добавления новых элементов. При нажатии на нее в модель с помощью метода append добавляется новый элемент. Это инициирует создание нового делегата в представлении и передачу сигнала GridView.onAdd. Анимация SequentialAnimation с именем addAnimation запускается из сигнала, который приводит к увеличению масштаба элемента путем анимации свойства scale делегата.

GridView.onAdd: addAnimation.start()

SequentialAnimation {
    id: addAnimation
    NumberAnimation { 
        target: wrapper
        property: "scale"
        from: 0
        to: 1
        duration: 250
        easing.type: Easing.InOutQuad 
    }
}

При клике на делегате в представлении элемент удаляется из модели посредством вызова метода remove. Это приводит к генерированию сигнала GridView.onRemove, запуская анимацию SequentialAnimation removeAnimation. Однако на этот раз уничтожение делегата должно быть отложено до завершения анимации. Для этого элемент PropertyAction используется для установки свойства GridView.delayRemove в значение true перед анимацией и false после. Это гарантирует, что анимация будет завершена до того, как элемент делегата будет удален.

GridView.onRemove: removeAnimation.start()

SequentialAnimation {
    id: removeAnimation
    
    PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: true }
    NumberAnimation { target: wrapper; property: "scale"; to: 0; duration: 250; easing.type: Easing.InOutQuad }
    PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: false }
}

Вот полный код примера:

import QtQuick

Rectangle {
    width: 480
    height: 300

    gradient: Gradient {
        GradientStop { position: 0.0; color: "#dbddde" }
        GradientStop { position: 1.0; color: "#5fc9f8" }
    }

    ListModel {
        id: theModel

        ListElement { number: 0 }
        ListElement { number: 1 }
        ListElement { number: 2 }
        ListElement { number: 3 }
        ListElement { number: 4 }
        ListElement { number: 5 }
        ListElement { number: 6 }
        ListElement { number: 7 }
        ListElement { number: 8 }
        ListElement { number: 9 }
    }

    Rectangle {
        property int count: 9

        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.margins: 20

        height: 40

        color: "#53d769"
        border.color: Qt.lighter(color, 1.1)

        Text {
            anchors.centerIn: parent

            text: "Add item!"
        }

        MouseArea {
            anchors.fill: parent

            onClicked: {
                theModel.append({"number": ++parent.count})
            }
        }
    }

    GridView {
        anchors.fill: parent
        anchors.margins: 20
        anchors.bottomMargin: 80

        clip: true

        model: theModel

        cellWidth: 45
        cellHeight: 45

        delegate: numberDelegate
    }

    Component {
        id: numberDelegate

        Rectangle {
            id: wrapper

            required property int index 
            required property int number

            width: 40
            height: 40

            gradient: Gradient {
                GradientStop { position: 0.0; color: "#f8306a" }
                GradientStop { position: 1.0; color: "#fb5b40" }
            }

            Text {
                anchors.centerIn: parent

                font.pixelSize: 10

                text: wrapper.number
            }

            MouseArea {
                anchors.fill: parent

                onClicked: {
                    if (wrapper.index == -1) {
                        return
                    }
                    theModel.remove(wrapper.index)
                }
            }
            
            GridView.onRemove: removeAnimation.start()
            
            SequentialAnimation {
                id: removeAnimation
                
                PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: true }
                NumberAnimation { target: wrapper; property: "scale"; to: 0; duration: 250; easing.type: Easing.InOutQuad }
                PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: false }
            }

            GridView.onAdd: addAnimation.start()
            
            SequentialAnimation {
                id: addAnimation
                NumberAnimation { 
                    target: wrapper
                    property: "scale"
                    from: 0
                    to: 1
                    duration: 250
                    easing.type: Easing.InOutQuad 
                }
            }
        }
    }
}

Делегаты, меняющие форму

Обычно используемый в списках механизм заключается в том, что текущий элемент расширяется при активации. Это можно использовать для динамического расширения элемента до заполнения экрана для входа в новую часть пользовательского интерфейса или для предоставления дополнительной информации для текущего элемента в заданном списке.

В приведенном ниже примере каждый элемент при клике раскрывается до полного размера ListView, содержащего его. Дополнительное пространство затем используется для добавления дополнительной информации. Механизм, используемый для управления этим, представляет собой состояние expanded, в которое может войти каждый делегат элемента, и в котором элемент расширяется. В этом состоянии изменяется ряд свойств.

Прежде всего, высота элемента wrapper устанавливается равной высоте ListView. Затем миниатюра изображения увеличивается и перемещается вниз, чтобы оно переместилось из маленького положения в большее. В дополнение к этому два скрытых элемента, factView и closeButton, отображаются путем изменения свойства opacity (непрозрачности) элементов. Наконец, настраивается ListView.

Настройка ListView включает установку contentY, то есть верхней части видимой части представления, в значение y делегата. Другое изменение заключается в том, чтобы установить свойство interactive представления в значение false. Это предотвращает перемещение представления. Пользователь больше не может прокручивать список или изменять текущий элемент.

Когда на элементе выполняется клик, он переходит в состояние expanded, в результате чего делегат элемента заполняет ListView, а содержимое переупорядочивается. При нажатии кнопки закрытия состояние очищается, в результате чего делегат возвращается в свое предыдущее состояние и повторно активирует ListView.

import QtQuick

Item {
    width: 300
    height: 480

    Rectangle {
        anchors.fill: parent
        gradient: Gradient {
            GradientStop { position: 0.0; color: "#4a4a4a" }
            GradientStop { position: 1.0; color: "#2b2b2b" }
        }
    }

    ListView {
        id: listView

        anchors.fill: parent

        delegate: detailsDelegate
        model: planets
    }

    ListModel {
        id: planets

        ListElement { name: "Mercury"; imageSource: "images/mercury.jpeg"; facts: "Mercury is the smallest planet in the Solar System. It is the closest planet to the sun. It makes one trip around the Sun once every 87.969 days." }
        ListElement { name: "Venus"; imageSource: "images/venus.jpeg"; facts: "Venus is the second planet from the Sun. It is a terrestrial planet because it has a solid, rocky surface. The other terrestrial planets are Mercury, Earth and Mars. Astronomers have known Venus for thousands of years." }
        ListElement { name: "Earth"; imageSource: "images/earth.jpeg"; facts: "The Earth is the third planet from the Sun. It is one of the four terrestrial planets in our Solar System. This means most of its mass is solid. The other three are Mercury, Venus and Mars. The Earth is also called the Blue Planet, 'Planet Earth', and 'Terra'." }
        ListElement { name: "Mars"; imageSource: "images/mars.jpeg"; facts: "Mars is the fourth planet from the Sun in the Solar System. Mars is dry, rocky and cold. It is home to the largest volcano in the Solar System. Mars is named after the mythological Roman god of war because it is a red planet, which signifies the colour of blood." }
    }

    Component {
        id: detailsDelegate

        Item {
            id: wrapper

            required property string name
            required property string imageSource
            required property string facts

            width: listView.width
            height: 30

            Rectangle {
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.top: parent.top

                height: 30

                color: "#333"
                border.color: Qt.lighter(color, 1.2)
                Text {
                    anchors.left: parent.left
                    anchors.verticalCenter: parent.verticalCenter
                    anchors.leftMargin: 4

                    font.pixelSize: parent.height-4
                    color: '#fff'

                    text: wrapper.name
                }
            }


            Rectangle {
                id: image

                width: 26
                height: 26

                anchors.right: parent.right
                anchors.top: parent.top
                anchors.rightMargin: 2
                anchors.topMargin: 2

                color: "black"


                Image {
                    anchors.fill: parent

                    fillMode: Image.PreserveAspectFit

                    source: wrapper.imageSource
                }
            }

            MouseArea {
                anchors.fill: parent
                onClicked: parent.state = "expanded"
            }

            Item {
                id: factsView

                anchors.top: image.bottom
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.bottom: parent.bottom

                opacity: 0

                Rectangle {
                    anchors.fill: parent

                    gradient: Gradient {
                        GradientStop { position: 0.0; color: "#fed958" }
                        GradientStop { position: 1.0; color: "#fecc2f" }
                    }
                    border.color: '#000000'
                    border.width: 2

                    Text {
                        anchors.fill: parent
                        anchors.margins: 5

                        clip: true
                        wrapMode: Text.WordWrap
                        color: '#1f1f21'

                        font.pixelSize: 12

                        text: wrapper.facts
                    }
                }
            }

            Rectangle {
                id: closeButton

                anchors.right: parent.right
                anchors.top: parent.top
                anchors.rightMargin: 2
                anchors.topMargin: 2

                width: 26
                height: 26

                color: "#157efb"
                border.color: Qt.lighter(color, 1.1)

                opacity: 0

                MouseArea {
                    anchors.fill: parent
                    onClicked: wrapper.state = ""
                }
            }

            states: [
                State {
                    name: "expanded"

                    PropertyChanges { target: wrapper; height: listView.height }
                    PropertyChanges { target: image; width: listView.width; height: listView.width; anchors.rightMargin: 0; anchors.topMargin: 30 }
                    PropertyChanges { target: factsView; opacity: 1 }
                    PropertyChanges { target: closeButton; opacity: 1 }
                    PropertyChanges { target: wrapper.ListView.view; contentY: wrapper.y; interactive: false }
                }
            ]

            transitions: [
                Transition {
                    NumberAnimation {
                        duration: 200;
                        properties: "height,width,anchors.rightMargin,anchors.topMargin,opacity,contentY"
                    }
                }
            ]
        }
    }
}
нормальное состояние
расширенное состояние

Продемонстрированные здесь методы расширения делегата для заполнения всего представления можно использовать для изменения формы делегата элемента гораздо меньшим образом. Например, при просмотре списка песен текущий элемент можно сделать немного больше, чтобы вместить больше информации об этом конкретном элементе.

Теги

GUI / Графический интерфейс пользователяMVD / model-view-delegate / модель-представление-делегатQMLQtQtQuickQtQuick ControlsДелегатПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.