Продвинутые методы использования архитектуры модель-представление-делегат
PathView
Элемент PathView
является наиболее гибким представлением в Qt Quick, но также и самым сложным. Он позволяет создать представление, в котором элементы располагаются по произвольному пути. По тому же пути можно детально контролировать такие атрибуты, как масштаб, непрозрачность и многое другое.
При использовании PathView
вы должны определить делегата и путь. В дополнение к этому сам PathView
можно настроить с помощью ряда свойств. Наиболее обычными являются pathItemCount
, контролирующее количество элементов, видимых одновременно, и свойства управления диапазоном выделения, preferrdHighlightBegin
, preferredHighlightEnd
и highlightRangeMode
, контролирующие, где на пути должен отображаться текущий элемент.
Прежде чем подробно рассмотреть свойства управления диапазоном выделения, мы должны рассмотреть свойство пути. Свойство path
ожидает элемент Path
, определяющий путь, по которому следуют делегаты при прокрутке PathView
. Путь определяется с помощью свойств startX
и startY
в сочетании с такими элементами пути, как PathLine
, PathQuad
и PathCubic
. Эти элементы соединяются вместе, образуя двухмерный путь.
Когда путь определен, его можно дополнительно настроить с помощью элементов PathPercent
и PathAttribute
. Они размещаются между элементами пути и обеспечивают более точное управление путем и делегатами на нем. PathPercent
определяет, насколько большая часть пути проходит между каждым элементом. Это, в свою очередь, контролирует распределение делегатов по пути, так как они распределяются пропорционально проценту прохождения.
Именно здесь вступают в игру свойства preferredHighlightBegin
и preferredHighlightEnd
объекта PathView
. Оба они ожидают реальных значений в диапазоне от нуля до единицы. Также ожидается, что конец будет больше или равен началу. Установив оба этих свойства, например, 0.5, текущий элемент будет отображаться на пятидесяти процентах пути.
В Path
элементы PathAttribute
размещаются между элементами, как и элементы PathPercent
. Они позволяют указать значения свойств, которые интерполируются по пути. Эти свойства присоединяются к делегатам и могут использоваться для управления любым мыслимым свойством.
В приведенном ниже примере показано, как элемент PathView
используется для создания представления карточек, которые пользователь может пролистывать. Для этого используется ряд приемов. Путь состоит из трех элементов PathLine
. Используя элементы PathPercent
, центральный элемент правильно центрируется и обеспечивает достаточно места, чтобы не загромождаться другими элементами. С помощью элементов PathAttribute
можно управлять поворотом, размером и z
-значением.
В дополнение к path
было задано свойство pathItemCount
объекта PathView
. Оно определяет, насколько плотно будет заполнен путь. preferredHighlightBegin
и preferredHighlightEnd
, а также PathView.onPath
используются для управления видимостью делегатов.
PathView {
anchors.fill: parent
model: 100
delegate: flipCardDelegate
path: Path {
startX: root.width / 2
startY: 0
PathAttribute { name: "itemZ"; value: 0 }
PathAttribute { name: "itemAngle"; value: -90.0; }
PathAttribute { name: "itemScale"; value: 0.5; }
PathLine { x: root.width / 2; y: root.height * 0.4; }
PathPercent { value: 0.48; }
PathLine { x: root.width / 2; y: root.height * 0.5; }
PathAttribute { name: "itemAngle"; value: 0.0; }
PathAttribute { name: "itemScale"; value: 1.0; }
PathAttribute { name: "itemZ"; value: 100 }
PathLine { x: root.width / 2; y: root.height * 0.6; }
PathPercent { value: 0.52; }
PathLine { x: root.width / 2; y: root.height; }
PathAttribute { name: "itemAngle"; value: 90.0; }
PathAttribute { name: "itemScale"; value: 0.5; }
PathAttribute { name: "itemZ"; value: 0 }
}
pathItemCount: 16
preferredHighlightBegin: 0.5
preferredHighlightEnd: 0.5
}
Делегат, показанный ниже, использует прикрепленные свойства itemZ
, itemAngle
и itemScale
из элементов PathAttribute
. Стоит отметить, что из wrapper
доступны только прикрепленные свойства делегата. Таким образом, свойство rotX
определено так, чтобы можно было получить доступ к значению из элемента Rotation
.
Еще одна деталь, характерная для PathView
, на которую стоит обратить внимание, – это использование прикрепленного свойства PathView.onPath
. Общепринятой практикой является привязка к нему видимости, так как это позволяет PathView
сохранять невидимые элементы для кэширования. Обычно это невозможно обработать путем отсечения, так как делегаты элементов в PathView
размещаются более свободно, чем делегаты элементов в представлениях ListView
или GridView
.
Component {
id: flipCardDelegate
BlueBox {
id: wrapper
required property int index
property real rotX: PathView.itemAngle
visible: PathView.onPath
width: 64
height: 64
scale: PathView.itemScale
z: PathView.itemZ
antialiasing: true
gradient: Gradient {
GradientStop { position: 0.0; color: "#2ed5fa" }
GradientStop { position: 1.0; color: "#2467ec" }
}
transform: Rotation {
axis { x: 1; y: 0; z: 0 }
angle: wrapper.rotX
origin { x: 32; y: 32; }
}
text: wrapper.index
}
}
При преобразовании изображений или других сложных элементов в PathView
обычно используется прием оптимизации производительности, заключающийся в привязке свойства smooth
элемента Image
к присоединенному свойству PathView.view.moving
. Это означает, что изображения менее красивы в движении, но плавно трансформируются в неподвижном состоянии. Нет смысла тратить вычислительную мощность на плавное масштабирование, когда представление находится в движении, так как пользователь всё равно этого не увидит.
Подсказка
Учитывая динамическую природу PathAttribute
, инструментарий qml (в данном случае: qmllint
) не знает ни itemZ
, ни itemAngle
, ни itemScale
.
При использовании PathView
и программном изменении currentIndex
может потребоваться управлять направлением движения пути. Это можно сделать с помощью свойства movementDirection
. Для него можно установить значение PathView.Shortest
, которое является значением по умолчанию. Это означает, что движение может быть в любом направлении, в зависимости от того, какой путь ближе всего к целевому значению. Направление также можно ограничить, установив для movementDirection
значение PathView.Negative
или PathView.Positive
.
Табличные модели
Все представления, обсуждавшиеся до сих пор, так или иначе, представляют собой массив элементов. Даже GridView
ожидает, что модель предоставит одномерный список элементов. Для двумерных таблиц данных вам необходимо использовать элемент TableView
.
TableView
похож на другие представления тем, что он объединяет модель с делегатом для формирования сетки. Если задана модель, ориентированная на список, то представление отображает один столбец, что делает его очень похожим на элемент ListView
. Однако TableView
также может отображать двумерные модели, которые явно определяют как столбцы, так и строки.
В приведенном ниже примере мы настроили простой TableView
с пользовательской моделью, предоставленной из C++. На данный момент невозможно создавать табличные модели непосредственно из QML, но в главе «Qt и C++» объясняется эта концепция. Рабочий пример показан на изображении ниже.
В приведенном ниже примере мы создаем TableView
и устанавливаем rowSpacing
и columnSpacing
для управления горизонтальными и вертикальными промежутками между делегатами. Остальные свойства настраиваются так же, как и для любого другого типа представления.
TableView {
id: view
anchors.fill: parent
anchors.margins: 20
rowSpacing: 5
columnSpacing: 5
clip: true
model: tableModel
delegate: cellDelegate
}
Сам делегат может передавать неявный размер через implicitWidth
и implicitHeight
. Это то, что мы делаем в примере ниже. Фактическое содержимое данных – данные, возвращенные ролью display
модели.
Component {
id: cellDelegate
GreenBox {
id: wrapper
required property string display
implicitHeight: 40
implicitWidth: 40
Text {
anchors.centerIn: parent
text: wrapper.display
}
}
}
В зависимости от содержимого модели можно предоставить делегаты разных размеров, например:
GreenBox {
implicitHeight: (1 + row) * 10
// ...
}
Обратите внимание, что ширина и высота должны быть больше нуля.
При предоставлении неявного размера из делегата размером управляют самый высокий делегат каждой строки и самый широкий делегат каждого столбца. Это может создать интересное поведение, если ширина элементов зависит от строки, или если высота зависит от столбца. Это связано с тем, что не все делегаты создаются постоянно, поэтому ширина столбца может меняться по мере того, как пользователь прокручивает таблицу.
Чтобы избежать проблем с указанием ширины столбцов и высоты строк с использованием неявных размеров делегатов, вы можете предоставить функции, вычисляющие эти размеры. Это делается с помощью columnWidthProvider
и rowHeightProvider
. Эти функции возвращают значения ширины и высоты соответственно, как показано ниже:
TableView {
columnWidthProvider: function (column) { return 10 * (column + 1) }
// ...
}
Если вам нужно динамически изменить ширину столбцов или высоту строк, вы должны уведомить представление об этом, вызвав метод forceLayout
. Это заставит представление пересчитать размер и положение всех ячеек.
Модель из XML
Поскольку XML является универсальным форматом данных, QML предоставляет элемент XmlListModel
, который предоставляет XML-данные в качестве модели. Этот элемент может извлекать данные XML локально или удаленно, а затем обрабатывать их с помощью выражений XPath.
Пример ниже демонстрирует получение изображений из потока RSS. Свойство source
ссылается на удаленное расположение по протоколу HTTP, и данные загружаются автоматически.
Когда данные загружены, они обрабатываются в элементы модели и роли. Свойство query
элемента XmlListModel
– это XPath, представляющий базовый запрос для создания элементов модели. В этом примере путь – /rss/channel/item, поэтому для каждого тега элемента, внутри тега канала, внутри тега RSS создается элемент модели.
Для каждого элемента модели извлекается ряд ролей. Они представлены элементами XmlListModelRole
. Каждой роли дается имя, к которому делегат может получить доступ через присоединенное свойство. Фактическое значение каждого такого свойства определяется через свойства elementName
и (необязательное) attributeName
для каждой роли. Например, свойство title
соответствует XML-элементу title
, возвращая содержимое между тегами <title>
и </title>
.
Свойство imageSource
извлекает значение атрибута тега вместо содержимого тега. В этом случае атрибут url
тега enclosure
извлекается как строка. Затем свойство imageSource
можно использовать непосредственно в качестве источника для элемента Image
, который загружает изображение с заданного URL-адреса.
import QtQuick
import QtQml.XmlListModel
import "../common"
Background {
width: 300
height: 480
Component {
id: imageDelegate
Box {
id: wrapper
required property string title
required property string imageSource
width: listView.width
height: 220
color: '#333'
Column {
Text {
text: wrapper.title
color: '#e0e0e0'
}
Image {
width: listView.width
height: 200
fillMode: Image.PreserveAspectCrop
source: wrapper.imageSource
}
}
}
}
XmlListModel {
id: imageModel
source: "https://www.nasa.gov/rss/dyn/image_of_the_day.rss"
query: "/rss/channel/item"
XmlListModelRole { name: "title"; elementName: "title" }
XmlListModelRole { name: "imageSource"; elementName: "enclosure"; attributeName: "url"; }
}
ListView {
id: listView
anchors.fill: parent
model: imageModel
delegate: imageDelegate
}
}
Списки с разделами
Иногда данные в списке могут быть разделены на разделы. Например, разделен может быть список контактов на разделы по каждой букве алфавита или музыкальные треки по альбомам. С помощью ListView
можно разделить плоский список на категории, что обеспечивает большую глубину взаимодействия.
Чтобы использовать разделы, необходимо настроить section.property
и section.criteria
. Свойство section.property
определяет, какое свойство использовать для разделения содержимого на разделы. Здесь важно знать, что модель должна быть отсортирована таким образом, чтобы каждый раздел состоял из непрерывно идущих элементов, иначе одно и то же имя свойства может появиться в нескольких местах.
Для section.criteria
можно задать либо ViewSection.FullString
, либо ViewSection.FirstCharacter
. Первый является значением по умолчанию и может использоваться для моделей, имеющих четкие разделы, например, треки музыкальных альбомов. Последний принимает первый символ свойства и означает, что для него можно использовать любое свойство. Наиболее распространенным примером для этого является фамилия контакта в телефонной книге.
Когда разделы определены, доступ к ним можно получить из каждого элемента с помощью прикрепленных свойств ListView.section
, ListView.previousSection
и ListView.nextSection
. Используя эти свойства, можно определить первый и последний элемент раздела и действовать соответствующим образом.
Также возможно назначить компонент делегата раздела свойству section.delegate
представления ListView
. Это создает делегат заголовка раздела, который вставляется перед любыми элементами раздела. Компонент делегата может получить доступ к имени текущего раздела, используя прикрепленное свойство section
.
Пример ниже демонстрирует концепцию разделов, показывая список космонавтов, разделенных на секции по их национальности. В качестве свойства section.property
используется nation
. Компонент section.delegate
, sectionDelegate
, показывает заголовок для каждого раздела, отображая название нации. В каждом разделе имена космонавтов отображаются с помощью компонента spaceManDelegate
.
import QtQuick
import "../common"
Background {
width: 300
height: 290
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: spaceMen
delegate: spaceManDelegate
section.property: "nation"
section.delegate: sectionDelegate
}
Component {
id: spaceManDelegate
Item {
id: spaceManWrapper
required property string name
width: ListView.view.width
height: 20
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 8
font.pixelSize: 12
text: spaceManWrapper.name
color: '#1f1f1f'
}
}
}
Component {
id: sectionDelegate
BlueBox {
id: sectionWrapper
required property string section
width: ListView.view ? ListView.view.width : 0
height: 20
text: sectionWrapper.section
fontColor: '#e0e0e0'
}
}
ListModel {
id: spaceMen
ListElement { name: "Abdul Ahad Mohmand"; nation: "Afganistan"; }
ListElement { name: "Marcos Pontes"; nation: "Brazil"; }
ListElement { name: "Alexandar Panayotov Alexandrov"; nation: "Bulgaria"; }
ListElement { name: "Georgi Ivanov"; nation: "Bulgaria"; }
ListElement { name: "Roberta Bondar"; nation: "Canada"; }
ListElement { name: "Marc Garneau"; nation: "Canada"; }
ListElement { name: "Chris Hadfield"; nation: "Canada"; }
ListElement { name: "Guy Laliberte"; nation: "Canada"; }
ListElement { name: "Steven MacLean"; nation: "Canada"; }
ListElement { name: "Julie Payette"; nation: "Canada"; }
ListElement { name: "Robert Thirsk"; nation: "Canada"; }
ListElement { name: "Bjarni Tryggvason"; nation: "Canada"; }
ListElement { name: "Dafydd Williams"; nation: "Canada"; }
}
}
ObjectModel
В некоторых случаях вы можете захотеть использовать представление списка для большого набора различных элементов. Вы можете решить эту проблему, используя динамический QML и загрузчик Loader
, но другой вариант – использовать ObjectModel
из модуля QtQml.Models
. Объектная модель отличается от других моделей тем, что позволяет размещать рядом с моделью фактические визуальные элементы. Таким образом, представлению не нужен делегат.
В приведенном ниже примере мы поместили три элемента Rectangle
в ObjectModel
. Однако один прямоугольник имеет дочерний элемент Text
, а последний имеет закругленные углы. Это привело бы к модели табличного стиля, использующей что-то вроде ListModel
. Это также привело бы к пустым элементам Text
в модели.
import QtQuick
import QtQml.Models
Rectangle {
width: 320
height: 320
gradient: Gradient {
GradientStop { position: 0.0; color: "#f6f6f6" }
GradientStop { position: 1.0; color: "#d7d7d7" }
}
ObjectModel {
id: itemModel
Rectangle { height: 60; width: 80; color: "#157efb" }
Rectangle { height: 20; width: 300; color: "#53d769"
Text { anchors.centerIn: parent; color: "black"; text: "Hello QML" }
}
Rectangle { height: 40; width: 40; radius: 10; color: "#fc1a1c" }
}
ListView {
anchors.fill: parent
anchors.margins: 10
spacing: 5
model: itemModel
}
}
Другой аспект ObjectModel
заключается в том, что ее можно динамически заполнять с помощью методов get
, insert
, move
, remove
и clear
. Таким образом, содержимое модели можно динамически генерировать из различных источников и по-прежнему легко отображать в одном представлении.
Модели с действиями
Тип ListElement
поддерживает привязку функций Javascript к свойствам. Это означает, что вы можете поместить функции в модель. Это очень полезно при создании меню с действиями и других подобных конструкций.
Пример ниже демонстрирует это, имея модель городов, которые приветствуют вас по-разному. actionModel
– это модель из четырех городов, но свойство hello
привязано к функциям. Каждая функция принимает значение аргумента, но у вас может быть любое количество аргументов.
В делегате actionDelegate
элемент MouseArea
вызывает функцию hello
как обычную функцию, что приводит к вызову соответствующего свойства hello
в модели.
import QtQuick
Rectangle {
width: 120
height: 300
gradient: Gradient {
GradientStop { position: 0.0; color: "#f6f6f6" }
GradientStop { position: 1.0; color: "#d7d7d7" }
}
ListModel {
id: actionModel
ListElement {
name: "Copenhagen"
hello: function(value) { console.log(value + ": You clicked Copenhagen!"); }
}
ListElement {
name: "Helsinki"
hello: function(value) { console.log(value + ": Helsinki here!"); }
}
ListElement {
name: "Oslo"
hello: function(value) { console.log(value + ": Hei Hei fra Oslo!"); }
}
ListElement {
name: "Stockholm"
hello: function(value) { console.log(value + ": Stockholm calling!"); }
}
}
ListView {
anchors.fill: parent
anchors.margins: 20
focus: true
model: actionModel
delegate: Rectangle {
id: actionDelegate
required property int index
required property string name
required property var hello
width: ListView.view.width
height: 40
color: "#157efb"
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: delegate.name
}
MouseArea {
anchors.fill: parent
onClicked: delegate.hello(delegate.index)
}
}
spacing: 5
clip: true
}
}
Настройка производительности
Воспринимаемая производительность представления модели очень сильно зависит от времени, необходимого для подготовки новых делегатов. Например, при прокрутке ListView
вниз делегаты добавляются сразу за пределами представления снизу и удаляются, как только они выходят из поля зрения выше представления. Это становится очевидным, если для свойства clip
установлено значение false
. Если для инициализации делегатов требуется слишком много времени, это станет заметным для пользователя, как только представление будет прокручиваться слишком быстро.
Чтобы обойти эту проблему, вы можете настроить поля отступа в пикселях по краям прокручиваемого представления. Это делается с помощью свойства cacheBuffer
. В случае, описанном выше, вертикальная прокрутка будет контролировать, сколько пикселей выше и ниже ListView
будут содержать подготовленные делегаты. Сочетание этого с асинхронной загрузкой элементов Image
может, например, дать изображениям время на загрузку, прежде чем они появятся в поле зрения.
Наличие большего количества делегатов приносит в жертву память для более плавного взаимодействия и немного больше времени для инициализации каждого делегата. Это не решает проблему сложных делегатов. Каждый раз, когда создается экземпляр делегата, его содержимое вычисляется и компилируется. Это требует времени, и если это займет слишком много времени, это приведет к плохому ощущению при прокрутке. Наличие большого количества элементов в делегате также ухудшит производительность прокрутки. На это просто тратятся циклы для перемещения большого количества элементов.
Чтобы исправить две последние проблемы, рекомендуется использовать элементы загрузчика Loader
. Их можно использовать для создания дополнительных элементов, когда они необходимы. Например, расширяющийся делегат может использовать Loader
, чтобы отложить создание подробного представления до тех пор, пока оно не понадобится. По той же причине лучше свести количество JavaScript в каждом делегате к минимуму. Лучше позволить им вызывать сложные фрагменты JavaScript, которые находятся вне каждого делегата. Это сокращает время, затрачиваемое на компиляцию JavaScript при каждом создании делегата.
Подсказка
Имейте в виду, что использование Loader
для отсрочки инициализации делает именно это – откладывает проблему с производительностью. Это означает, что производительность прокрутки будет улучшена, но для отображения фактического содержимого всё равно потребуется время.