Модель у нас отвечает за доступ к данным. Модель может быть реализована как в самом QML, так и на C++. Выбор тут больше всего зависит от того, где находится источник данных. Если в качестве источника данных используется код на C++, то там удобнее сделать и модель. Если же данные поступают напрямую в QML (например получаются из сети при помощи XMLHttpRequest), то лучше и модель реализовать на QML. Иначе придется передавать данные в C++, чтобы затем обратно их получать для отображения, что только усложнит код.
По тому, как модели реализуются, я разделю их на три категории:
- модели на C++;
- модели на QML;
- модели на JavaScript.
JavaScript-модели я вынес в отдельную категорию, т.к. у них есть определенные особенности, про них я расскажу чуть позже.
Начнем рассмотрение с моделей, реализованных средствами QML.
Model-View в QML:
- Model-View в QML. Часть нулевая, вводная
- Model-View в QML. Часть первая: Представления на основе готовых компонентов
- Model-View в QML. Часть вторая: Кастомные представления
- Model-View в QML. Часть третья: Модели в QML и JavaScript
1. ListModel
Это достаточно простой и, в то же время, функциональный компонент. Элементы в ListModel можно как определять статически (это продемонстрировано в первом примере), так и добавлять/удалять динамически (соответственно, во втором примере). Разберем оба способа подробнее.
1) Статический
Когда мы определяем элементы модели статически, нам нужно определить данные в дочерних элементах, которые имеют тип ListElement и определяются внутри модели. Данные определяются в свойствах объекта ListElement и доступны как роли в делегате.
При статическом определении данных в ListModel, типы данных, которые можно записать в ListElement весьма ограничены. По сути, все данные должны быть константами. Т.е. можно использовать строки или числа, а вот объект или функцию использовать не получится. В таком случае вы получите ошибку «ListElement: cannot use script for property value». Но можно использовать список, элементами которого будут все те же объекты ListElement.
import QtQuick 2.0
Rectangle {
width: 360
height: 240
ListModel {
id: dataModel
ListElement {
color: "orange"
texts: [
ListElement { text: "one" },
ListElement { text: "two" }
]
}
ListElement {
color: "skyblue"
texts: [
ListElement { text: "three" },
ListElement { text: "four" }
]
}
}
ListView {
id: view
anchors.margins: 10
anchors.fill: parent
spacing: 10
model: dataModel
delegate: Rectangle {
width: view.width
height: 100
color: model.color
Row {
anchors.margins: 10
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 10
Repeater {
model: texts
delegate: Text {
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
text: model.text
}
}
}
}
}
}
Роль texts мы используем внутри делегата в качестве модели, таким образом можно достичь нескольких уровней вложенности.
В результате мы получим примерно такое:
Еще один важный момент. В статически описанной модели во всех объектах ListElement каждая роль должна хранить данные только одного типа. Т.е. нельзя в одном элементе записать в нее число, а в другом строку. Например, рассмотрим немного измененную модель из самого первого примера:
ListModel {
id: dataModel
ListElement {
color: "orange"
text: 1
}
ListElement {
color: "skyblue"
text: "second"
}
}
Мы получим такую ошибку: «Can't assign to existing role 'text' of different type [String -> Number]» и вместо текста во втором делегате получим 0.
2) Динамический
Этот способ дает нам гораздо больше возможностей, чем статический. Не все они описаны в документации и могут быть очевидными, поэтому рассмотрим их поподробнее.
Интерфейс для манипуляции элементами в ListModel похож на интерфейс обычного списка. Элементы можно добавлять/удалять/перемещать, можно получать их значение и заменять или редактировать.
ListModel принимает значение элемента в виде JavaScript-объекта. Соответственно, свойства этого объекта станут ролями в делегате.
Если взять самый первый пример, то модель можно переписать так, чтобы она наполнялась динамически:
ListModel {
id: dataModel
Component.onCompleted: {
append({ color: "orange", text: "first" })
append({ color: "skyblue", text: "second" })
}
}
Объект можно задавать не только литералом, а передать переменную, которая этот объект содержит:
var value = {
color: "orange",
text: "first"
}
append(value)
Когда я писал про статическое наполнение, я сказал, что типы данных, которые можно поместить в модель должны быть константами. У меня есть хорошая новость :) Когда мы наполняем модель динамически, эти ограничения не действуют. Мы можем в качестве значения свойства и массивы, и объекты. Даже функции, но с небольшими особенностями. Возьмем все этот же пример и немного его перепишем:
QtObject {
id: obj
function alive() {
console.log("It's alive!")
}
}
ListModel {
id: dataModel
Component.onCompleted: {
var value
value = {
data: {
color: "orange",
text: "first"
},
functions: obj
}
append(value)
value = {
data: {
color: "skyblue",
text: "second"
},
functions: obj
}
append(value)
}
}
Поскольку мы поместили свойства color и text в объект data, то в делегате они будут как свойства этого объекта, т.е. model.data.color.
С функциями немного сложнее. Если мы просто сделаем свойство в объекте и присвоим ему функцию, то внутри делегата мы увидим, что эта функция превратилась в пустой объект. Но если использовать тип QtObject, то внутри него все сохраняется и ничего не пропадает. Так что в определении компонента мы можем добавить такую строчку:
Component.onCompleted: model.functions.alive()
и эта функция вызовется после создания компонента.
Помещение функций в данные больше походит на хак и я рекомендую не сильно увлекаться такими вещами, а вот помещение объектов в модель очень нужная вещь. Например, если приходят данные из сети прямо в QML (при помощи XMLHttpRequest) в формате JSON (а при работе с веб-ресурсами обычно так и происходит), то декодировав JSON, мы получим JavaScript-объект, который можно просто добавить в ListModel.
Я уже писал про то, что во всех статически определенных элементах ListModel роли должны быть одних и тех же типов. По умолчанию, для элементов, добавляемых в ListModel динамически это правило тоже действует. И первый добавленный элемент определяет, какого типа будут роли. Но в Qt 5 добавилась возможность сделать типы ролей динамическими. Для этого нужно установить у ListModel свойство dynamicRoles в true.
ListModel {
id: dataModel
dynamicRoles: true
Component.onCompleted: {
append({ color: "orange", text: "first" })
append({ color: "skyblue", text: 2 })
}
}
Удобная штука, но есть пару важных моментов, которые стоит помнить. Ценой за такое удобство является производительность — разработчики Qt утверждают, что она будет в 4-6 раз меньше. Кроме того, динамические типы ролей не будут работать у модели со статически определенными элементами.
Еще один очень важный момент. Первый добавляемый в модель элемент определяет не только типы ролей, но и какие роли вообще в модели будут. Если в нем какие-то роли отсутствуют, то их потом не получится добавить. Но есть одно исключение. Если элементы добавляются на этапе создания модели (т.е. в обработчике Component.onCompleted), то в итоге у модели будут все роли, которые были во всех этих элементах.
Возьмем второй пример и немного его переделаем так, чтобы при создании модели добавлялся один элемент без свойства text, а затем по нажатию на кнопку будем добавлять элементы с текстом «new».
import QtQuick 2.0
Rectangle {
width: 360
height: 360
ListModel {
id: dataModel
dynamicRoles: true
Component.onCompleted: {
append({ color: "orange" })
}
}
Column {
anchors.margins: 10
anchors.fill: parent
spacing: 10
ListView {
id: view
width: parent.width
height: parent.height - button.height - parent.spacing
spacing: 10
model: dataModel
clip: true
delegate: Rectangle {
width: view.width
height: 40
color: model.color
Text {
anchors.centerIn: parent
renderType: Text.NativeRendering
text: model.text || "old"
}
}
}
Rectangle {
id: button
width: 100
height: 40
anchors.horizontalCenter: parent.horizontalCenter
border {
color: "black"
width: 1
}
Text {
anchors.centerIn: parent
renderType: Text.NativeRendering
text: "Add"
}
MouseArea {
anchors.fill: parent
onClicked: dataModel.append({ color: "skyblue", text: "new" })
}
}
}
}
В результате, у всех новых элементов текста не будет и будет в качестве текста «old»:
Перепишем определение модели и добавим на этапе создания еще один элемент со свойством text, но без свойства color:
ListModel {
id: dataModel
Component.onCompleted: {
append({ color: "orange" })
append({ text: "another old" })
}
}
Подправим определении делегата, чтобы использовался цвет по умолчанию, если он не указан:
color: model.color || "lightgray"
В итоге модель сформирована с обеими ролями и при добавлении новых элементов все отображается так, как задумано:
Мы также можем комбинировать статическое и динамической наполнение модели. Но использование статического способа накладывает все его ограничения и динамически мы сможем добавлять только объекты с ролями тех же типов.
Небольшая новость: в Qt 5.1 эта модель вынесена из QtQuick в отдельный модуль QtQml.Models. Чтобы ее использовать, надо подключить этот модуль:
import QtQml.Models 2.1
Но бросаться все переписывать не обязательно —для совместимости с существующем кодом модель будет доступна и в модуле QtQuick.
ListModel можно считать QML-версией моделей из Qt. Она имеет похожий функционал, позволяет манипулировать данными и является активной моделью. Могу сказать, что в QML это наиболее функциональный и удобный компонент для создания моделей.
2. VisualItemModel (ObjectModel)
Архитектура Model-View фреймворка Qt выделяет две основных сущности: модель и представление и одну вспомогательную — делегат. Поскольку представление здесь является контейнером для экземпляров делегата, то делегат обычно определяется там же.
Этот компонент позволяет перенести делегат из представления в саму модель. Реализуется это тем, что в модель помещаются не данные, а уже готовые визуальные элементы. Соответственно, представлению в таком случае делегат не нужен и оно используется только как контейнер, обеспечивая позиционирование элементов и навигацию по ним.
Одной интересной особенностью VisualItemModel является то, что в нее можно положить объекты разный типов. Обычная модель с делегатом использует для отображения всех данных объекты одного и того же типа. Когда требуется отображать в одном представлении элементы разных типов, такая модель является одним из вариантов решения данной проблемы.
В качестве примера, поместим в модель объекты типов Rectangle и Text и отобразим их при помощи ListView:
import QtQuick 2.0
Rectangle {
width: 360
height: 240
VisualItemModel {
id: itemModel
Rectangle {
width: view.width
height: 100
color: "orange"
}
Text {
width: view.width
height: 100
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
text: "second"
}
}
ListView {
id: view
anchors.margins: 10
anchors.fill: parent
spacing: 10
model: itemModel
}
}
В Qt 5.1 эта модель вынесена из QtQuick в отдельный модуль QtQml.Models и называется ObjectModel. Точно также, как и с ListModel, для использования этой модели надо подключить соответствующий модуль. Интерфейс остался тот же, достаточно просто заменить VisualDataModel на ObjectModel.
Модель будет все также доступна и через VisualDataModel, чтобы не ломать совместимость со старым кодом. Но если разрабатывать под новую версию, лучше сразу использовать новое название.
3. XmlListModel
При работе с веб-ресурсами нередко применяется формат XML. В частности, он используется в таких вещах, как RSS, XSPF, различных подкастах и т.п. А значит, у нас появляется задача получить этот файл и его распарсить. Еще XML может содержать список элементов (например список песен в случае XSPF), из которых нам нужно будет создать модель. Перебирать дерево элементов и наполнять модель вручную не самый удобный способ, так что нужна возможность задать выбрать элементы из XML-файла автоматически и представить их в виде модели. Эти задачи и реализует XmlListModel.
От нас требуется указать адрес XML-файла, указать критерий, по которому нужно отобрать элементы и определить, какие роли должны быть видны в делегате. В качестве критерия для отбора элементов мы пишем запрос в формате XPath. Для ролей делегата мы указываем тоже XPath-запрос, на основании которого из элемента будут получены данные для роли. Для простых случаев, вроде разбора RSS, эти запросы тоже будут простыми и по сути описывают путь в XML-файле. Я не буду здесь углубляться в дебри XPath и если вам пока не особо понятно, что это за зверь, я рекомендую почитать соответствующий раздел в документации по Qt. Здесь же я буду использовать примеры, которые не делают никакой хитрой выборки, так что я надеюсь, что все будет достаточно понятно.
В качестве примера, мы получим RSS-фид Хабра и отобразим заголовки статей.
Rectangle {
width: 360
height: 360
color: "lightsteelblue"
XmlListModel {
id: dataModel
source: "http://habrahabr.ru/rss/hubs/"
query: "/rss/channel/item"
XmlRole {
name: "title"
query: "title/string()"
}
}
ListView {
id: view
anchors.margins: 10
anchors.fill: parent
spacing: 10
model: dataModel
delegate: Rectangle {
width: view.width
height: 40
radius: 10
Text {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
wrapMode: Text.Wrap
renderType: Text.NativeRendering
text: model.title
}
}
}
}
Нужные нам элементы — это блоки , который вложены в , а тот в свою очередь в . Из этого пути мы конструируем наше первое выражение XPath. У нас будет всего одна роль, содержащая заголовок статьи. Чтобы его получить, нужно у элемента взять и привести его в строку. Из этого мы и формируем второе выражение XPath. На этом формирование модели закончено, осталось только ее отобразить. В итоге мы получим примерно такой результат:
Эта модель вынесена в отдельный модуль, для ее использования, надо дополнительно подключать этот модуль:
import QtQuick.XmlListModel 2.0
4. FolderListModel
Для многих приложений совсем не лишним будет доступ к файловой системе. В QML есть для этого экспериментальный компонент, представляющий каталог файловой системы в виде модели — FileSystemModel. Чтобы его использовать, надо подключит одноименный модуль:
import Qt.labs.folderlistmodel 1.0
Пока он экспериментальный, он входит в Qt Labs, но в будущем его могут переместить в Qt Quick или куда-нибудь еще.
Для того, чтобы использовать модель нам надо, в первую очередь, задать каталог при помощи свойства folder. Путь надо задавать в формате URL, т.е. путь к каталог у файловой системы задается через «file:». Можно указать путь для ресурсов при помощи «qrc:».
Можно задать фильтры для имен файлов при помощи свойства nameFilters, принимающего список масок для отбора нужных файлов. Можно настраивать также попадание в модель каталогов и сортировку файлов.
Для примера, получим список файлов в каталоге и выведем информацию об этих файлах в виде таблицы:
import QtQuick 2.0
import QtQuick.Controls 1.0
import Qt.labs.folderlistmodel 1.0
Rectangle {
width: 600
height: 300
FolderListModel {
id: dataModel
showDirs: false
nameFilters: [
"*.jpg",
"*.png"
]
folder: "file:///mnt/store/Pictures/Wallpapers"
}
TableView {
id: view
anchors.margins: 10
anchors.fill: parent
model: dataModel
clip: true
TableViewColumn {
width: 300
title: "Name"
role: "fileName"
}
TableViewColumn {
width: 100
title: "Size"
role: "fileSize"
}
TableViewColumn {
width: 100
title: "Modified"
role: "fileModified"
}
itemDelegate: Item {
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
renderType: Text.NativeRendering
text: styleData.value
}
}
}
}
Мы убираем из модели каталоги и оставляем только файлы *.jpg и *.png.
С этой моделью у делегата в качестве данных доступна информация о файле: путь, имя и т.п. Мы используем здесь имя, размер и время модификации.
К файловой системе мы доступ получать научились. Но смотреть на имена картинок может быть не так чтобы уж очень захватывающе, так что в качестве бонуса сделаем чуть более интересное их отображение :) Мы уже рассматривали такую вещь, как CoverFlow. Самое время тут ее применить.
Итак, возьмем пример CoverFlow и немного его поменяем. Модель мы возьмем из предыдущего примера. Увеличим размер элемента:
property int itemSize: 400
И поменяем делегата:
delegate: Image {
property real rotationAngle: PathView.angle
property real rotationOrigin: PathView.origin
width: itemSize
height: width
z: PathView.z
fillMode: Image.PreserveAspectFit
source: model.filePath
transform: Rotation {
axis { x: 0; y: 1; z: 0 }
angle: rotationAngle
origin.x: rotationOrigin
}
}
Ну а теперь посмотрим на прикольную штуку, которая у нас получилось:
FolderListModel — очень полезный компонент, дающий нам доступ к файловой системе и, несмотря на свою экспериментальность, его вполне можно использовать уже сейчас.
5. JavaScript-модели
Помимо специально разработанных для создания моделей компонентов, немало других объектов может также выступать в качестве модели. И такой вариант может даже получится проще, чем использование для модели специальных компонентов.
В основном, такие модели получаются пассивными, и подходят, когда количество элементов фиксированное или редко меняется.
Мы рассмотрим такие типы в качестве модели:
- списки/массивы;
- объекты JavaScript и QML-компоненты;
- целые числа.
1) Списки/массивы
Можно использовать обыкновенные JavaScript-массивы в качестве модели. Для каждого элемента массива будет создан делегат и данные самого элемент массива будут доступны в делегате через свойство modelData.
import QtQuick 2.0
Rectangle {
property var dataModel: [
{
color: "orange"
},
{
color: "skyblue",
text: "second"
}
]
width: 360
height: 240
ListView {
id: view
anchors.margins: 10
anchors.fill: parent
spacing: 10
model: dataModel
delegate: Rectangle {
width: view.width
height: 100
color: modelData.color
Text {
anchors.centerIn: parent
renderType: Text.NativeRendering
text: modelData.text || "empty text"
}
}
}
}
Если в массиве находятся объекты, то modelData тоже будет объектом и будет содержать все свойства исходного объекта. Если в качестве элементов будут простые значения, то они и будут в качестве modelData. Например:
property var dataModel: [
"orange",
"skyblue"
]
и в делегате обращаемся к данным модели так:
color: modelData
И точно также как и в ListModel, мы можем в данные модели поместить функцию. Как и в случае с ListModel, если ее поместить в обычный JavaScript-объект, то в делегате она будет видна как пустой объект. Поэтому здесь тоже используем трюк с QtObject.
property var dataModel: [
{
color: "orange",
functions: obj
},
{
color: "skyblue",
text: "second",
functions: obj
}
]
QtObject {
id: obj
function alive() {
console.log("It's alive!")
}
}
И в делегате вызываем функцию:
Component.onCompleted: modelData.functions.alive()
Я уже говорил, что почти все JavaScript-модели являются пассивными и эта не исключение. При изменении элементов и их добавлении/удалении представление не будет знать, что они поменялись. Так происходит потому, что у свойств JavaScript-объектов нет сигналов, которые вызываются при изменении свойства, в отличие от Qt-объектов и, соответственно QML-объектов. Представление получит сигнал, если мы изменим само свойство, используемое в качестве модели, заменим модель. Но тут есть одна хитрость: мы можем не только присвоить этому свойству новую модель но и переприсвоить старую. Например:
dataModel.push({ color: "skyblue", text: "something new" })
dataModel = dataModel
Такая модель хорошо подходит для данных, которые поступают с веб-ресурсов и обновляются редко и/или полностью.
2) объекты
JavaScript-объекты и объекты QML могут выступать моделью. У этой модели будет один элемент и свойства объекта будут ролями в делегате.
Возьмем самый первый пример и переделаем для использовании JavaScript-объекта в качестве модели:
property var dataModel: null
Component.onCompleted: {
dataModel = {
color: "orange",
text: "some text"
}
}
Свойства объекта в делегате доступны через modelData:
color: modelData.color
Как и с JavaScript-массивами, изменение объекта после того, как он был установлен в качестве модели никак не влияет на отображение, т.е. это тоже пассивная модель.
К JavaScript-моделям я отнес и использование одного QML-объекта в качестве модели. Хотя эти объекты могут использоваться как полноценная QML-модель, по функциональности это почти аналог использования обычного JavaScript-объекта, с некоторыми особенностями. Поэтому я и рассматриваю их вместе.
Поменяем тот же пример для использования в качестве модели QML-объекта:
Item {
id: dataModel
property color color: "orange"
property string text: "some text"
}
Item здесь выбран чтобы показать, что в качестве модели может быть любой QML-объект. На практике, если нужно хранить только данные, то лучше всего подойдет QtObject. Это самый базовый и, соответственно, самый легкий QML-объект. Item же, в данном случае, содержит слишком много лишнего.
У такой модели данные в делегате доступны как через model, так и через modelData.
Также, эта модель является единственной активной из JavaScript-моделей. Поскольку у свойств QML-объектов есть сигналы, вызывающиеся при изменении свойства, то изменение свойства в объекте приведет к изменению данных в делегате.
3) Целое число
Самая простая модель :) Мы можем в качестве модели использовать целое число. Это число является количеством элементов модели.
property int dataModel: 5
Или можно напрямую указать в качестве модели константу:
model: 5
В делегате будет доступно свойство modelData, которое содержит индекс. Индекс также будет доступен через model.index.
Такая модель хорошо подойдет, когда надо создать некоторое количество одинаковых элементов.
В качестве вывода
Мы рассмотрели модели, которые реализуются средствами QML и JavaScript. Вариантов много, но от себя скажу, что наиболее часто используемые — это ListModel и JavaScript-массивы.
Рассмотренные модели реализуются достаточно просто, если нам не требуются какие-то особые хитрости (вроде хранения функций в ListModel). В тех случаях, где такой вариант подходит, мы можем реализовать все компоненты MVC на одном языке и тем самым уменьшить сложность программы.
Но, я хочу обратить внимание на одну вещь. Не стоит все тащить все в QML, стоит руководствоваться практическими соображениями. Некоторые вещи может быть проще реализовать на C++. Именно C++-модели мы рассмотрим в следующей части.
Автор: BlackRaven86