29 September 2013

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

ProgrammingQtDesigning and refactoring
Модель у нас отвечает за доступ к данным. Модель может быть реализована как в самом 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
  5. Model-View в QML. Часть четвертая: C++-модели


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++-модели мы рассмотрим в следующей части.
Tags: qt qt5 qml qt quick model view controller mvc
Hubs: Programming Qt Designing and refactoring
+18
40.5k 130
Comments 2
Ads
Top of the day