Model-View in QML. Part Three: Models in QML and JavaScript

    Our model is responsible for data access. The model can be implemented both in QML itself and in C ++. The choice here most depends on where the data source is located. If C ++ code is used as a data source, then it is more convenient to make a model there. If the data comes directly to QML (for example, it is obtained from the network using XMLHttpRequest), then it is better to implement the model in QML. Otherwise, you will have to transfer the data to C ++, then to receive it back for display, which will only complicate the code.

    By the way the models are implemented, I will divide them into three categories:
    • C ++ models;
    • QML models
    • JavaScript models.

    I put JavaScript models into a separate category, as they have certain features, I will talk about them a little later.
    We begin our discussion with models implemented using QML.

    Model-View in QML:
    1. Model-View in QML. Part zero, introductory
    2. Model-View in QML. Part One: Prebuilt Component Views
    3. Model-View in QML. Part Two: Custom Views
    4. Model-View in QML. Part Three: Models in QML and JavaScript
    5. Model-View in QML. Part Four: C ++ Models


    1. ListModel

    This is a fairly simple and, at the same time, functional component. Elements in the ListModel can be defined both statically (this is demonstrated in the first example) and dynamically added / removed (respectively, in the second example). We will analyze both methods in more detail.

    1) Static

    When we define model elements statically, we need to define data in child elements that are of type ListElement and are defined inside the model. Data is defined in the properties of the ListElement and is available as roles in the delegate.
    When statically defining data in a ListModel, the types of data that can be written to a ListElement are very limited. In fact, all data should be constants. Those. You can use strings or numbers, but an object or function cannot be used. In this case, you will get the error “ListElement: cannot use script for property value”. But you can use a list whose elements are all the same ListElement objects.

    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
                        }
                    }
                }
            }
        }
    }
    

    We use the role of texts inside the delegate as a model, thus several levels of nesting can be achieved.
    As a result, we get something like this:



    Another important point. In a statically described model, in all ListElement objects, each role should store data of only one type. Those. it is impossible to write a number in one element and a line in another. For example, consider a slightly modified model from the very first example:

    ListModel {
        id: dataModel
        ListElement {
            color: "orange"
            text: 1
        }
        ListElement {
            color: "skyblue"
            text: "second"
        }
    }
    

    We get this error: “Can't assign to existing role 'text' of different type [String -> Number]” and instead of text in the second delegate we get 0.

    2) Dynamic

    This method gives us much more than static. Not all of them are described in the documentation and may be obvious, so we will consider them in more detail.

    The interface for manipulating elements in a ListModel is similar to the interface of a regular list. Elements can be added / deleted / moved, you can get their value and replace or edit.

    ListModel takes the value of an element as a JavaScript object. Accordingly, the properties of this object will become roles in the delegate.
    If we take the very first example, then the model can be rewritten so that it is filled dynamically:

    ListModel {
        id: dataModel
        Component.onCompleted: {
            append({ color: "orange", text: "first" })
            append({ color: "skyblue", text: "second" })
        }
    }
    

    An object can be set not only by a literal, but by passing a variable that this object contains:

    var value = {
        color: "orange",
        text: "first"
    }
    append(value)
    

    When I wrote about static filling, I said that the types of data that can be placed in the model should be constants. I have some good news :) When we fill the model dynamically, these restrictions do not apply. We can use arrays and objects as the value of the property. Even features, but with small features. Let's take the same example and rewrite it a bit:

    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)
        }
    }
    

    Since we placed the color and text properties in the data object, in the delegate they will be like the properties of this object, i.e. model.data.color.

    With features a little trickier. If we just make a property in an object and assign a function to it, then inside the delegate we will see that this function has turned into an empty object. But if you use the QtObject type, then everything is saved inside it and nothing disappears. So in the component definition we can add the following line:

    Component.onCompleted: model.functions.alive()
    

    and this function will be called after the component is created.

    Putting functions into data is more like a hack and I recommend not to get too carried away with such things, but putting objects in a model is a very necessary thing. For example, if data comes from the network directly to QML (using XMLHttpRequest) in JSON format (and when working with web resources, this usually happens), then decoding JSON will result in a JavaScript object that can be simply added to the ListModel.

    I already wrote about the fact that in all statically defined ListModel elements, roles must be of the same types. By default, for elements added to the ListModel dynamically, this rule also applies. And the first item added determines what type of roles will be. But Qt 5 added the ability to make role types dynamic. To do this, set the ListModel dynamicRoles property to true.

    ListModel {
        id: dataModel
        dynamicRoles: true
        Component.onCompleted: {
        append({ color: "orange", text: "first" })
        append({ color: "skyblue", text: 2 })
        }
    }
    

    A handy thing, but there are a couple of important points to remember. The price for such convenience is performance - Qt developers claim that it will be 4-6 times less. In addition, dynamic role types will not work on models with statically defined elements.

    Another very important point. The first element added to the model determines not only the types of roles, but also what roles will be in the model in general. If there are no roles in it, then they cannot be added later. But there is one exception. If elements are added at the stage of model creation (i.e., in the Component.onCompleted handler), then in the end the model will have all the roles that were in all these elements.

    Let's take the second example and remodel it a bit so that when creating the model one element is added without the text property, and then by clicking on the button we will add elements with the 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" })
                }
            }
        }
    }
    

    As a result, all new elements of the text will not have and will be “old” as the text:



    We rewrite the definition of the model and add at the stage of creation one more element with the text property, but without the color property:

    ListModel {
        id: dataModel
        Component.onCompleted: {
            append({ color: "orange" })
            append({ text: "another old" })
        }
    }
    

    We tweak the delegate definition to use the default color if it is not specified:

    color: model.color || "lightgray"
    

    As a result, the model is formed with both roles and when adding new elements everything is displayed as intended:



    We can also combine static and dynamic content models. But using the static method imposes all its limitations and dynamically we can add only objects with roles of the same types.

    Little news: in Qt 5.1, this model is moved from QtQuick to a separate module QtQml.Models. To use it, you need to connect this module:

    import QtQml.Models 2.1
    

    But rushing to rewrite everything is not necessary — for compatibility with existing code, the model will also be available in the QtQuick module.

    ListModel can be considered a QML version of models from Qt. It has similar functionality, allows you to manipulate data and is an active model. I can say that in QML this is the most functional and convenient component for creating models.

    2. VisualItemModel (ObjectModel)

    The Model-View architecture of the Qt framework distinguishes two main entities: a model and a view, and one auxiliary, a delegate. Since the view here is a container for delegate instances, the delegate is usually defined there.

    This component allows you to transfer a delegate from the view to the model itself. This is realized by the fact that data is not placed in the model, but ready-made visual elements. Accordingly, in this case, the delegate is not needed for the presentation and it is used only as a container, providing positioning of elements and navigation on them.

    One interesting feature of VisualItemModel is that you can put objects of different types into it. The regular delegate model uses objects of the same type to display all the data. When you want to display elements of different types in one view, this model is one of the solutions to this problem.

    As an example, we will place objects of types Rectangle and Text in the model and display them using the 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
        }
    }
    

    In Qt 5.1, this model is removed from QtQuick in a separate module QtQml.Models and is called ObjectModel. Just like with ListModel, to use this model you need to connect the appropriate module. The interface remains the same, just replace VisualDataModel with ObjectModel.

    The model will also be available through VisualDataModel, so as not to break compatibility with old code. But if you develop for the new version, it is better to immediately use the new name.

    3. XmlListModel

    When working with web resources, the XML format is often used. In particular, it is used in such things as RSS, XSPF, various podcasts, etc. So, we have a task to get this file and parse it. XML may also contain a list of elements (for example, a list of songs in the case of XSPF) from which we will need to create a model. Going through the tree of elements and filling the model manually is not the most convenient way, so you need to be able to set elements to be selected from the XML file automatically and present them as a model. These tasks are implemented by XmlListModel.

    We are required to indicate the address of the XML file, indicate the criteria by which to select the elements, and determine which roles should be visible in the delegate. As a criterion for selecting elements, we write a query in XPath format. For delegate roles, we also specify an XPath request based on which data for the role will be obtained from the element. For simple cases, like RSS parsing, these requests will also be simple and essentially describe the path in the XML file. I will not go deeper into the jungle of XPath here, and if you still don’t really understand what kind of animal it is, I recommend reading the corresponding section in the Qt documentation. Here I will use examples that do not make any tricky selection, so I hope that everything will be clear enough.

    As an example, we’ll get the Habr’s RSS feed and display article titles.

    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
                }
            }
        }
    }
    

    The elements we need are the blocks that are nested in, and that in turn is in. From this path, we construct our first XPath expression. We will have only one role containing the title of the article. To get it, you need to take it from the element and bring it to a string. From this we form the second XPath expression. This completes the formation of the model, it remains only to display it. As a result, we get something like this:



    This model is placed in a separate module, for its use, you must additionally connect this module:

    import QtQuick.XmlListModel 2.0
    

    4. FolderListModel

    For many applications, access to the file system will not be out of place. QML has an experimental component for this, representing the file system directory as a model - FileSystemModel. To use it, you need to connect the module of the same name:

    import Qt.labs.folderlistmodel 1.0
    

    While it is experimental, it is part of Qt Labs, but in the future it can be moved to Qt Quick or somewhere else.
    In order to use the model, we first need to set the directory using the folder property. The path must be specified in URL format, i.e. the path to the directory of the file system is set via "file:". You can specify the path for resources using "qrc:".

    You can set filters for file names using the nameFilters property, which accepts a list of masks for selecting the necessary files. You can also configure getting into the directory model and sorting files.

    For example, we get a list of files in a directory and display information about these files in a table:

    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
                }
            }
        }
    }
    



    We remove directories from the model and leave only * .jpg and * .png files.

    With this model, the delegate has access to file information as a data: path, name, etc. We use here the name, size and modification time.

    We learned how to access the file system. But looking at the names of the pictures may not be so exciting, so as a bonus we’ll make them a little more interesting to display :) We have already considered such a thing as CoverFlow. It's time to apply it here.

    So, let's take an example of CoverFlow and change it a bit. We will take the model from the previous example. Increase the element size:

    property int itemSize: 400
    

    And change the delegate:

    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
        }
    }
    

    Well, now let's look at a cool thing that we got:



    FolderListModel is a very useful component that gives us access to the file system and, despite its experimental nature, it can be used right now.

    5. JavaScript models

    In addition to specially designed components for creating models, many other objects can also act as models. And this option may even turn out easier than using special components for the model.

    Basically, such models are passive, and are suitable when the number of elements is fixed or rarely changes.

    We will consider such types as a model:

    • lists / arrays;
    • JavaScript objects and QML components
    • whole numbers.

    1) Lists / arrays


    You can use ordinary JavaScript arrays as a model. A delegate will be created for each element of the array and the data of the array element itself will be available in the delegate via the modelData property.

    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"
                }
            }
        }
    }
    

    If there are objects in the array, then modelData will also be an object and will contain all the properties of the original object. If there are simple values ​​as elements, then they will be as modelData. For instance:

    property var dataModel: [
        "orange",
        "skyblue"
    ]
    

    and in the delegate we turn to the model data like this:

    color: modelData
    

    And just like in the ListModel, we can put a function in the model data. As with ListModel, if you place it in a regular JavaScript object, then in the delegate it will be visible as an empty object. Therefore, here we also use the trick with QtObject.

    property var dataModel: [
        {
            color: "orange",
            functions: obj
        },
        {
            color: "skyblue",
            text: "second",
            functions: obj
        }
    ]
    QtObject {
        id: obj
        function alive() {
            console.log("It's alive!")
        }
    }
    

    And in the delegate, we call the function:

    Component.onCompleted: modelData.functions.alive()
    

    I have already said that almost all JavaScript models are passive and this is no exception. When elements are changed and added / deleted, the view will not know that they have changed. This is because the properties of JavaScript objects do not have signals that are called when the property changes, unlike Qt objects and, accordingly, QML objects. The view will receive a signal if we change the property itself used as a model, replace the model. But there is one trick: we can not only assign a new model to this property but also reassign the old one. For instance:

    dataModel.push({ color: "skyblue", text: "something new" })
    dataModel = dataModel
    

    Such a model is well suited for data that comes from web resources and is rarely updated and / or completely.

    2) objects

    JavaScript objects and QML objects can act as models. This model will have one element and the properties of the object will be roles in the delegate.
    Take the very first example and remake it to use a JavaScript object as a model:

    property var dataModel: null
    Component.onCompleted: {
        dataModel = {
            color: "orange",
            text: "some text"
        }
    }
    

    The object properties in the delegate are accessible through modelData:

    color: modelData.color
    

    As with JavaScript arrays, changing an object after it has been set as a model does not affect the display, i.e. it is also a passive model.

    I also referred to the use of JavaScript models as a single QML object as a model. Although these objects can be used as a full-fledged QML model, in terms of functionality it is almost an analogue of using a regular JavaScript object, with some features. Therefore, I consider them together.

    Change the same example to use as a model of a QML object:

    Item {
        id: dataModel
        property color color: "orange"
        property string text: "some text"
    }
    

    Item is selected here to show that any QML object can be a model. In practice, if you only want to store data, then QtObject is best. This is the most basic and, accordingly, the lightest QML object. Item, in this case, contains too much superfluous.

    With such a model, the data in the delegate is available both through model and through modelData.

    Also, this model is the only active of the JavaScript models. Since properties of QML objects have signals that are triggered when a property changes, changing a property in the object will cause the data in the delegate to change.

    3) integer

    The simplest model :) We can use an integer as the model. This number is the number of model elements.

    property int dataModel: 5
    

    Or you can directly specify a constant as a model:

    model: 5
    

    The delegate will have access to the modelData property, which contains the index. The index will also be available through model.index.

    Such a model is well suited when you need to create a number of identical elements.

    As a conclusion

    We examined models that are implemented using QML and JavaScript. There are many options, but on my own behalf I will say that the most commonly used are ListModel and JavaScript arrays.

    The considered models are implemented quite simply if we do not need any special tricks (like storing functions in a ListModel). In those cases where this option is suitable, we can implement all MVC components in one language and thereby reduce the complexity of the program.

    But, I want to pay attention to one thing. It’s not worth it to drag everything into QML; you should be guided by practical considerations. Some things may be easier to implement in C ++. Namely C ++ - models we will consider in the next part.

    Also popular now: