Делегаты в QML
Когда дело доходит до использования моделей и представлений в пользовательском интерфейсе, делегат играет огромную роль в создании внешнего вида и поведения. Поскольку каждый элемент модели визуализируется через делегата, то, что на самом деле видно пользователю, – это делегаты.
Каждый делегат получает доступ к ряду прикрепленных свойств, некоторые из модели данных, другие из представления. Из модели свойства передают делегату данные для каждого элемента. Из представления свойства передают информацию о состоянии, связанную с делегатом в представлении. Давайте пройдемся по свойствам из представления.
Наиболее часто используемые свойства, прикрепленные из представления, – это 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
}
}
}
}
Если каждый элемент в модели связан с действием, например, клик по элементу влияет на него, эта функциональность является частью каждого делегата. Это разделяет управление событиями между представлением, которое обрабатывает навигацию между элементами в представлении, и делегатом, который обрабатывает действия над конкретным элементом.
Самый простой способ сделать это – создать 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"
}
}
]
}
}
}
Продемонстрированные здесь методы расширения делегата для заполнения всего представления можно использовать для изменения формы делегата элемента гораздо меньшим образом. Например, при просмотре списка песен текущий элемент можно сделать немного больше, чтобы вместить больше информации об этом конкретном элементе.