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
    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++-модели мы рассмотрим в следующей части.
    • +18
    • 37.3k
    • 2
    Share post

    Similar posts

    Comments 2

      +5
      Спасибо за статью, качественного материала в сети про QML по-настоящему мало.
      Но не смотря на все плюсы, ListView в QtQuick это просто какая-то настоящая сингулярность. Чего только стоит сделать полноценный drag'n'drop между вьюхами для моделей. Внутри всё настолько оптимизировано и подогнано под конкретные юзкейсы, что любой шаг в сторону и кранты.
      image
        +1
        Drag'n'drop еще не использовал. А что там конкретно не работает?

      Only users with full accounts can post comments. Log in, please.