Динамические представления в QML
Повторители (элементы Repeater
) хорошо работают с ограниченными и статическими наборами данных, но в реальном мире модели обычно более сложны и больше по размеру. Здесь требуется более умное решение. Для этого Qt Quick предоставляет элементы ListView
и GridView
. Оба они основаны на области Flickable
, поэтому пользователь может перемещаться по большему набору данных. В то же время они ограничивают количество одновременно создаваемых делегатов. Для большой модели это означает уменьшение количества элементов, отображаемых одновременно в сцене.
Эти два элемента схожи в своем использовании. Мы начнем с ListView
, а затем опишем GridView
в сравнении с первым. Обратите внимание, что GridView
помещает список элементов в двумерную сетку слева направо или сверху вниз. Если вы хотите отобразить таблицу данных, вам нужно использовать TableView
, который описан в разделе «Модели таблиц».
ListView
похож на элемент Repeater
. Он использует model
, создает экземпляр делегата (delegate
), и между делегатами может быть интервал (spacing
). В приведенном ниже примере показано, как это может выглядеть.
import QtQuick
import "../common"
Background {
width: 80
height: 300
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 100
delegate: GreenBox {
required property int index
width: 40
height: 40
text: index
}
spacing: 5
}
}
Если модель содержит больше данных, чем может уместиться на экране, ListView
отображает только часть списка. Однако, как следствие поведения Qt Quick по умолчанию, ListView
не ограничивает область экрана, в которой отображаются делегаты. Это означает, что делегаты могут быть видны за пределами представления списка, а динамическое создание и уничтожение делегатов за пределами представления списка видно пользователю. Чтобы предотвратить это, необходимо активировать отсечение в элементе ListView
, установив для свойства clip
значение true
. На приведенном ниже рисунке показан результат этого (слева) по сравнению со случаем, когда свойство clip
имеет значение false
(справа).
Для пользователя ListView
представляет собой прокручиваемую область. Он поддерживает кинетическую прокрутку, что означает, что его можно быстро перемещать по содержимому. По умолчанию он также может растягиваться за пределы содержимого, а затем возвращаться назад, чтобы сообщить пользователю, что конец достигнут.
Поведение в конце представления управляется с помощью свойства boundsBehavior
. Это перечислимое значение, которое может принимать следующие варианты:
Flickable.DragAndOvershootBounds
(поведение по умолчанию), при котором представление можно как перетаскивать, так и пролистывать за свои границы;Flickable.StopAtBounds
, при котором представление никогда не будет выходить за свои границы;Flickable.DragOverBounds
(золотая середина) позволяет пользователю перетаскивать представление за его границы, но клики останавливаются на границе.
Позиции, в которых представление может останавливаться, можно ограничить. Это управляется с помощью свойства snapMode
. Поведение по умолчанию, ListView.NoSnap
, позволяет останавливать представление в любой позиции. Установив для свойства snapMode
значение ListView.SnapToItem
, представление всегда будет выравнивать верх элемента со своим верхом. Наконец, ListView.SnapOneItem
, представление остановится на не более чем одном элементе от первого видимого элемента, когда кнопка мыши или касание были отпущены. Последний режим очень удобен при перелистывании страниц.
Ориентация
Представление в виде списка по умолчанию предоставляет список с вертикальной прокруткой, но горизонтальная прокрутка может быть столь же полезной. Направление ListView
контролируется через свойство orientation
. Для него можно задать либо значение по умолчанию, ListView.Vertical
, либо ListView.Horizontal
. Горизонтальный список показан ниже.
import QtQuick
import "../common"
Background {
width: 480
height: 80
ListView {
anchors.fill: parent
anchors.margins: 20
spacing: 4
clip: true
model: 100
orientation: ListView.Horizontal
delegate: GreenBox {
required property int index
width: 40
height: 40
text: index
}
}
}
Как вы можете заметить, по умолчанию направление горизонтального потока – слева направо. Его можно контролировать с помощью свойства layoutDirection
, которое может быть установлено либо в Qt.LeftToRight
, либо в Qt.RightToLeft
, в зависимости от направления потока.
Навигация с помощью клавиатуры и выделение
При использовании ListView
на сенсорных устройствах достаточно самого представления. В сценарии с клавиатурой или даже просто клавишами со стрелками для выбора элемента необходим механизм для указания текущего элемента. В QML это называется выделением (highlighting).
Представления поддерживают делегат выделения, который отображается в представлении вместе с делегатами. Его можно считать дополнительным делегатом, но он создается только один раз и перемещается в ту же позицию, что и текущий элемент.
Это показано в примере ниже. Для этого необходимы два свойства. Во-первых, чтобы свойство focus
было установлено значение true
. Это дает ListView
фокус клавиатуры. Во-вторых, свойство highlight
указывает используемого делегата выделения. Делегат выделения получает координаты (x
, y)
и высоту (height
) текущего элемента. Если ширина (width
) не указана, также используется ширина текущего элемента.
В примере для ширины используется присоединенное свойство ListView.view.width
. Присоединенные свойства, доступные для делегатов, обсуждаются далее в разделе о делегатах данной главы, но полезно знать, что те же самые свойства доступны и для делегатов выделения.
import QtQuick
import "../common"
Background {
width: 240
height: 300
ListView {
id: view
anchors.fill: parent
anchors.margins: 20
focus: true
model: 100
delegate: numberDelegate
highlight: highlightComponent
spacing: 5
clip: true
}
Component {
id: highlightComponent
GreenBox {
width: ListView.view ? ListView.view.width : 0
}
}
Component {
id: numberDelegate
Item {
id: wrapper
required property int index
width: ListView.view ? ListView.view.width : 0
height: 40
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: wrapper.index
}
}
}
}
При использовании выделения вместе с ListView
можно использовать ряд свойств для управления его поведением. highlightRangeMode
управляет тем, как на выделение влияет то, что отображается в представлении:
- Параметр по умолчанию
ListView.NoHighlightRange
означает, что выделение и видимый диапазон элементов в представлении вообще не связаны. - Значение
ListView.StrictlyEnforceRange
гарантирует, что выделение всегда будет видимым. Если действие попытается переместить выделение за пределы видимой части представления, текущий элемент изменится соответствующим образом, поэтому выделение останется видимым. - Золотая середина – это значение
ListView.ApplyRange
. Оно пытается сохранить выделение видимым, но не изменяет текущий элемент, чтобы обеспечить это. Вместо этого выделенному фрагменту разрешается перемещаться из поля зрения, если это необходимо.
В конфигурации по умолчанию представление отвечает за перемещение выделения в нужное положение. Скорость движения и изменение размера можно контролировать либо как скорость, либо как продолжительность. Используемые свойства: highlightMoveSpeed
, highlightMoveDuration
, highlightResizeSpeed
и highlightResizeDuration
. По умолчанию скорость установлена на 400 пикселей в секунду, а продолжительность установлена на -1, что указывает на то, что скорость и расстояние определяют продолжительность. Если установлены и скорость, и продолжительность, выбирается то, что обеспечивает самую быструю анимацию.
Чтобы более детально управлять движением выделения, можно для свойства highlightFollowCurrentItem
установить значение false
. Это означает, что представление больше не отвечает за перемещение делегата выделения. Вместо этого движением можно управлять с помощью Behavior
или анимации.
В приведенном ниже примере свойство y
делегата выделения привязано к присоединенному свойству ListView.view.currentItem.y
. Это гарантирует, что выделение следует за текущим элементом. Однако, поскольку мы не позволяем представлению перемещать выделение, мы можем контролировать, как перемещается этот элемент. Это делается через Behavior on y
. В приведенном ниже примере движение разделено на три этапа: исчезновение, перемещение, появление. Обратите внимание, как для создания более сложного движения можно использовать элементы SequentialAnimation
и PropertyAnimation
в сочетании с NumberAnimation
.
Component {
id: highlightComponent
Item {
width: ListView.view ? ListView.view.width : 0
height: ListView.view ? ListView.view.currentItem.height : 0
y: ListView.view ? ListView.view.currentItem.y : 0
Behavior on y {
SequentialAnimation {
PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 0; duration: 200 }
NumberAnimation { duration: 1 }
PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 1; duration: 200 }
}
}
GreenBox {
id: highlightRectangle
anchors.fill: parent
}
}
}
«Шапка» (header) и «подвал» (footer)
В каждый конец содержимого ListView
можно вставить элементы header
и footer
. Их можно считать специальными делегатами, помещенными в начало или конец списка. Для горизонтального списка они будут отображаться не вверху или в внизу, а в начале или в конце, в зависимости от используемого значения layoutDirection
.
В приведенном ниже примере показано, как можно использовать header
и footer
для улучшения восприятия начала и конца списка. Есть и другие способы использования этих специальных элементов списка. Например, их можно использовать для хранения кнопок для загрузки большего количества контента.
import QtQuick
import "../common"
Background {
width: 240
height: 300
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 4
delegate: numberDelegate
header: headerComponent
footer: footerComponent
spacing: 2
}
Component {
id: headerComponent
YellowBox {
width: ListView.view ? ListView.view.width : 0
height: 20
text: 'Header'
}
}
Component {
id: footerComponent
YellowBox {
width: ListView.view ? ListView.view.width : 0
height: 20
text: 'Footer'
}
}
Component {
id: numberDelegate
GreenBox {
required property int index
width: ListView.view.width
height: 40
text: 'Item #' + index
}
}
}
Подсказка
Делегаты header
и footer
не учитывают свойство spacing
(интервал между элементами) ListView
, вместо этого они размещаются непосредственно рядом со следующим делегатом элемента в списке. Это означает, что любой интервал должен быть частью элементов верхнего и нижнего колонтитула.
GridView
Использование GridView
очень похоже на использование ListView
. Единственное реальное отличие состоит в том, что представление сетки помещает делегатов в двумерную сетку, а не в линейный список.
По сравнению с представлением списка, представление сетки не зависит от интервала и размера своих делегатов. Вместо этого оно использует свойства cellWidth
и cellHeight
для управления размерами делегатов контента. Затем каждый элемент делегата помещается в верхний левый угол каждой такой ячейки.
import QtQuick
import "../common"
Background {
width: 220
height: 300
GridView {
id: view
anchors.fill: parent
anchors.margins: 20
clip: true
model: 100
cellWidth: 45
cellHeight: 45
delegate: GreenBox {
required property int index
width: 40
height: 40
text: index
}
}
}
GridView
содержит «шапку» и «подвал», может использовать делегат выделения и поддерживает режимы привязки, а также различные варианты поведения границ. Он также может быть ориентирован в различных направлениях и ориентациях.
Ориентация управляется с помощью свойства flow
. Для него можно задать значение GridView.LeftToRight
или GridView.TopToBottom
. Первое значение заполняет сетку слева направо, добавляя строки сверху вниз. Представление можно прокручивать в вертикальном направлении. Последнее значение добавляет элементы сверху вниз, заполняя представление слева направо. Направление прокрутки в этом случае горизонтальное.
В дополнение к свойству flow
свойство layoutDirection
может адаптировать направление сетки для языков слева направо или справа налево, в зависимости от используемого значения.