Model-View в QML. Часть третья: Модели в QML и JavaScript

от автора

Модель у нас отвечает за доступ к данным. Модель может быть реализована как в самом QML, так и на C++. Выбор тут больше всего зависит от того, где находится источник данных. Если в качестве источника данных используется код на C++, то там удобнее сделать и модель. Если же данные поступают напрямую в QML (например получаются из сети при помощи XMLHttpRequest), то лучше и модель реализовать на QML. Иначе придется передавать данные в C++, чтобы затем обратно их получать для отображения, что только усложнит код.

По тому, как модели реализуются, я разделю их на три категории:

  • модели на C++;
  • модели на QML;
  • модели на JavaScript.

JavaScript-модели я вынес в отдельную категорию, т.к. у них есть определенные особенности, про них я расскажу чуть позже.
Начнем рассмотрение с моделей, реализованных средствами QML.

Model-View в QML:

  1. Model-View в QML. Часть нулевая, вводная
  2. Model-View в QML. Часть первая: Представления на основе готовых компонентов
  3. Model-View в QML. Часть вторая: Кастомные представления
  4. 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++-модели мы рассмотрим в следующей части.

ссылка на оригинал статьи http://habrahabr.ru/post/195706/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *