По тому, как модели реализуются, я разделю их на три категории:
- модели на 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++-модели мы рассмотрим в следующей части.
ссылка на оригинал статьи http://habrahabr.ru/post/195706/
Добавить комментарий