📜 ⬆️ ⬇️

Model-View in QML. Part Four: C ++ Models

Since the main purpose of QML is to create interfaces, in accordance with the MVC template, it implements representation and control. For the implementation of the model, C ++ is completely logical. Here we will have much less restrictions and we will be able to implement a model of any complexity. In addition, if a significant part of the program is written in C ++ and the data comes from there, then it is best to place the model in the same place.


From the use of such a model can scare the apparent complexity of implementation. I will not argue with the fact that C ++ is not the easiest language. It is more complicated QML and requires more care not to shoot yourself in the foot, it is a fact. Despite this, in practice, not everything is so scary.


First, let's not forget that we do not write in pure C ++, but using Qt. Such things as parent-child in QObject, implicit sharing for containers, signals and slots, QVariant and many other things greatly simplify and automate the work with memory, which saves the developer from the mass of headaches and increases reliability. Sometimes you even get the impression that you write in a dynamic programming language. This also reduces the gap between QML and C ++, making the transition between them more or less smooth.


Secondly, all QML models ultimately lead to these C ++ models, only we get a simplified version and not the maximum speed. If you already have an understanding of how to work with models on QML, then it will be easier to handle C ++ models. We will simply learn a little more low-level information in the process, at the same time understanding how it all works will improve.


In general, mastering C ++ models is very much worth it. This applies in particular to QAbstractItemModel, with which we will begin.


Model-View in QML:



1. C ++ - QAbstractItemModel Model


This is the standard model from the Qt Model-View framework. This class has rich capabilities and allows you to build models of varying complexity.


There are three base classes for such models. QAbstractTableModel represents the data in the form of a table, the row and column numbers are used to access the data. QAbstractListModel presents the data in the form of a list and, one may say, is a special case of the previous model with one column.


QAbstractItemModel, on the contrary, is a more generalized version. Each element of the table can also have children, also organized as a table. Thus, using this table, you can organize a tree structure. In Qt, there is an accepted rule that child elements can only have elements of the first column and using Qt views such as QTreeView needs just such a format, but no one forbids organizing a model as conveniently as possible. As an example of such a model, you can use the QFileSystemModel class. The first column is the file or directory names. Items in this column can also have children if this is a directory. The remaining columns contain various information about the file - size, modification time, etc. This data structure can be found in any file manager:



Between the model and the presentation, you can insert a special proxy model. Such models intercept calls to the main model and can hide certain elements, change their order, influence the acquisition and recording of data, etc. Qt has a ready-made QSortFilterProxyModel class that can represent model data in sorted and / or filtered form. If its functionality is not enough, you can create your own proxy model, inheriting from this class or from QAbstractProxyModel.


Views in QML can only display lists. With the help of VisualDataModel you can navigate through the tree structure, but we can only display items of the current level. If the data needs to be stored in a tree and at the same time displayed in QML, then you should either use VisualDataModel or write your own proxy model that will turn this tree into a list.


In order to create our own model, we need to inherit from one of the base classes for the models and determine the required methods for this model. I will describe briefly what needs to be done, more information can be obtained in the documentation . We will consider in order of increasing complexity.


For the list model, you need to create a derived class from QAbstractListModel and define such methods:



This is sufficient if you do not plan to edit the model data with the help of the delegate. Edited model will be discussed a little later.


For the table model, another columnCount () method is added, which returns the number of columns. Table views in QML use elements from the first column and, when displayed, assign the roles of this element as columns. Thus, the table in QML is implemented using the same list and the tabular model is hardly worth using.


If we need a model with a tree structure, we use QAbstractItemModel. This model will need to further define the following functions:



In Qt models, accessing elements goes through special indexes - objects of type QModelIndex. They contain the row and column number, the index of the parent element and some additional data. The root element of the model has an invalid QModelIndex () index. So if we have a simple list or table - all elements have a parent element like this. In the case of a tree, only the top-level elements will have such a parent. The index () function obtains the index of the parent and the row and column number of the element; it must return the index of the element. Indexes are created using the createIndex () function.


In fact, difficulties begin when we need nesting, and everything is quite simple.


As an example, consider the model list. The data will be stored in the same object as a list of strings. We will also make the add () function, which will add another element to the model and mark it with the special macro Q_INVOKABLE so that it can be called from QML.


Class definition:


#include <QAbstractListModel> #include <QStringList> class TestModel : public QAbstractListModel { Q_OBJECT public: enum Roles { ColorRole = Qt::UserRole + 1, TextRole }; TestModel(QObject *parent = 0); virtual int rowCount(const QModelIndex &parent) const; virtual QVariant data(const QModelIndex &index, int role) const; virtual QHash<int, QByteArray> roleNames() const; Q_INVOKABLE void add(); private: QStringList m_data; }; 

We define the two roles ColorRole and TextRole and use values ​​for them greater than Qt :: UserRole - this is where the reserved values ​​for Qt end. Accordingly, for user roles it is necessary to use values ​​starting from Qt :: UserRole.


Implementing class methods:


 TestModel::TestModel(QObject *parent): QAbstractListModel(parent) { m_data.append("old"); m_data.append("another old"); } int TestModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return m_data.size(); } QVariant TestModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } switch (role) { case ColorRole: return QVariant(index.row() < 2 ? "orange" : "skyblue"); case TextRole: return m_data.at(index.row()); default: return QVariant(); } } QHash<int, QByteArray> TestModel::roleNames() const { QHash<int, QByteArray> roles = QAbstractListModel::roleNames(); roles[ColorRole] = "color"; roles[TextRole] = "text"; return roles; } void TestModel::add() { beginInsertRows(QModelIndex(), m_data.size(), m_data.size()); m_data.append("new"); endInsertRows(); m_data[0] = QString("Size: %1").arg(m_data.size()); QModelIndex index = createIndex(0, 0, static_cast<void *>(0)); emit dataChanged(index, index); } 

Because QML accesses roles using string names instead of integer constants, we define names for them: color and text. Before adding, we call the special function beginInsertRows (), which will emit the necessary signals so that the view is aware of what the addition of elements is about and where they will be added. And after, we call the endInsertRows () function, which again will emit signals that elements have been added to the model. All additions need to be wrapped in this way. There are similar functions for removing and moving items.


In the add () function, we change the text of the first element so that it shows the number of elements in the list. After that, we issue a dataChanged () signal to inform the viewer about this. The signal is transmitted by the parameters of the initial and final index of the changed data (we have the same). The index is obtained using the createIndex () function, which is passed the parameters of a row, column, and a pointer to private data. As the latter, a pointer to an object with data is usually used, but in our case, you can simplify and always use NULL.


As a QML program, let's rewrite the second example. C ++ - the model is implemented as a plug-in (plugin). At the beginning of the file, add its import:


 import TestModel 1.0 

Create an object of this type and use it as a model:


 TestModel { id: dataModel } 

After launching the program and adding several elements, we get something like this:



To edit the model data in the delegate, a standard interface is provided and to use it, it is necessary to override the setData () method in our model. The ability to edit QAbstractItemModel data from QML appeared in Qt 5.


Add to the header file such declarations:


 virtual bool setData(const QModelIndex &index, const QVariant &value, int role); virtual Qt::ItemFlags flags(const QModelIndex &index) const; 

and in the definition implementation file:


 bool TestModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) { return false; } switch (role) { case ColorRole: return false; // This property can not be set case TextRole: m_data[index.row()] = value.toString(); break; default: return false; } emit dataChanged(index, index, QVector<int>() << role); return true; } Qt::ItemFlags TestModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::ItemIsEnabled; return QAbstractListModel::flags(index) | Qt::ItemIsEditable; } 

We added to the model the ability to edit the text property directly from the delegate using similar code:


 model.text = "Some new text" 

Edit the delegate from our example and add the following element to it:


 MouseArea { anchors.fill: parent onDoubleClicked: model.text = "Edited" } 

Now with a double click on the element, its text will change to "Edited".


The Qt :: ItemIsEditable flag is used for Qt mappings to indicate that an item can be edited, so the flags () method must be overridden. At present, this flag is not checked in QML and the model will be editable without installing it, but I would recommend not to neglect it, since In future versions, checking for this may be added.


2. C ++ lists


As a model, you can use lists of strings or objects of type QObject.


Let's make a simple class with a property of type QStringList:


 #include <QObject> #include <QStringList> class TestModel : public QObject { Q_OBJECT Q_PROPERTY(QStringList data READ data CONSTANT) public: TestModel(QObject *parent = 0); QStringList data() const; }; TestModel::TestModel(QObject *parent): QObject(parent) { } QStringList TestModel::data() const { return QStringList() << "orange" << "skyblue"; } 

We use a slightly modified first example. Importing and creating a model object is exactly the same as in the previous example. But instead of the object itself, its property is used as a model:


 model: dataModel.data 

and the element index is used as text:


 text: model.index 

Such a list works just like a JavaScript array. Accordingly, this is a passive model and the addition / deletion of elements does not affect the representation.


3. QQmlListProperty


This class allows you to make a list that can be filled in both C ++ and QML. Filling in QML is performed statically when an object is created (as is done with a ListModel). In C ++, you can add / remove elements, so if you make a special method and mark it with the Q_INVOKABLE macro, you can also do it from QML.


Lists of this type can store objects of type QObject and types derived from it. In the type it is necessary to determine all the properties that will be used (using Q_PROPERTY).


Consider an example of such an object.


 #include <QObject> class Element : public QObject { Q_OBJECT Q_PROPERTY(QString color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) public: explicit Element(QObject *parent = 0); QString color() const; void setColor(QString color); QString text() const; void setText(QString text); signals: void colorChanged(QString color); void textChanged(QString text); private: QString m_color; QString m_text; }; Element::Element(QObject *parent) : QObject(parent) { } QString Element::color() const { return m_color; } void Element::setColor(QString color) { if (m_color == color) { return; } m_color = color; emit colorChanged(m_color); } QString Element::text() const { return m_text; } void Element::setText(QString text) { if (m_text == text) { return; } m_text = text; emit textChanged(m_text); } 

We created a simple class containing two properties — color and text, getters, setters, and notifiers for them.


In order for objects of this type to be used in a QQmlListProperty, this type must be visible in QML. To do this, you need to register this type using the qmlRegisterType () function. I use the C ++ plugin, so I register this type in a special handler, along with the model:


 void TestModelPlugin::registerTypes(const char *uri) { qmlRegisterType<TestModel>(uri, 1, 0, "TestModel"); qmlRegisterType<Element>(uri, 1, 0, "Element"); } 

In order to use QQmlListProperty, you need to create a property of type QQmlListProperty in some object, where T is the type of objects to be stored. In our case, there will be a property of type QQmlListProperty.

The QQmlListProperty constructor takes as arguments the methods that the QML engine will invoke when working with the list. These are methods for adding and retrieving an item, getting the number of items, and clearing the list. Only the first is obligatory, but it is better to define them all.


So, the class code of our model:


 #include <QObject> #include <QQmlListProperty> class Element; class TestModel : public QObject { Q_OBJECT Q_PROPERTY(QQmlListProperty<Element> data READ data NOTIFY dataChanged) Q_CLASSINFO("DefaultProperty", "data") public: TestModel(QObject *parent = 0); QQmlListProperty<Element> data(); Q_INVOKABLE void add(); signals: void dataChanged(); private: static void appendData(QQmlListProperty<Element> *list, Element *value); static int countData(QQmlListProperty<Element> *list); static Element *atData(QQmlListProperty<Element> *list, int index); static void clearData(QQmlListProperty<Element> *list); QList<Element*> m_data; }; TestModel::TestModel(QObject *parent): QObject(parent) { Element *element = new Element(this); element->setProperty("color", "lightgreen"); element->setProperty("text", "eldest"); m_data << element; } QQmlListProperty<Element> TestModel::data() { return QQmlListProperty<Element>(static_cast<QObject *>(this), static_cast<void *>(&m_data), &TestModel::appendData, &TestModel::countData, &TestModel::atData, &TestModel::clearData); } void TestModel::add() { Element *element = new Element(this); element->setProperty("color", "skyblue"); element->setProperty("text", "new"); m_data.append(element); emit dataChanged(); } void TestModel::appendData(QQmlListProperty<Element> *list, Element *value) { QList<Element*> *data = static_cast<QList<Element*> *>(list->data); data->append(value); } int TestModel::countData(QQmlListProperty<Element> *list) { QList<Element*> *data = static_cast<QList<Element*> *>(list->data); return data->size(); } Element *TestModel::atData(QQmlListProperty<Element> *list, int index) { QList<Element*> *data = static_cast<QList<Element*> *>(list->data); return data->at(index); } void TestModel::clearData(QQmlListProperty<Element> *list) { QList<Element*> *data = static_cast<QList<Element*> *>(list->data); qDeleteAll(data->begin(), data->end()); data->clear(); } 

As in the example with QAbstractItemModel, there is a add () method for adding items and an item is also added to the constructor.


In the data () method, an object of type QQmlListProperty is created. In the constructor, he gets a parent (QObject), a pointer to private data, which will be available in the functions for working with the list and the functions themselves. In all functions, the first argument is a pointer to an object of type QQmlListProperty which, in the data property, contains our private data. I put there a list in which Element objects are actually stored.


The signal for the data property is needed so that when adding / removing objects in the course of the presentation, they will receive information about changes in the model. After such a signal, the display will reread the entire model.


To demonstrate this model, take a slightly modified second example.


Connect C ++ plugin:


 import ListProperty_Plugin 1.0  : TestModel { id: dataModel data: [ Element { color: "orange" text: "old" }, Element { color: "lightgray" text: "another old" } ] } 

The data property is defined as a regular list. Since we registered the type Element, such objects can now be created in QML. It is worth noting that the definition of elements of the data array here does not replace those elements that already exist. These elements will be added to the one defined in the constructor of the TestModel class.


As a model, not the TestModel object itself is used, but the same data property:


 model: dataModel.data 

The data in the delegate is available through modelData:


 color: modelData.color 

and


 text: modelData.text 

Elements can be added to the data property only statically, so we use the add () function we write for this:


 onClicked: dataModel.add() 

As a result, we get something like this:



In the TestModel class, we specified data as the default property (using the Q_CLASSINFO directive). This allows us to define Element objects directly in the TestModel object and they themselves will be added to the desired property. So you can simplify the definition of a model and rewrite it like this:


 TestModel { id: dataModel Element { color: "orange" text: "old" } Element { color: "lightgray" text: "another old" } } 

Thus, using QQmlListProperty, you can implement the active model without using the QAbstractItemModel classes. If a large amount of data is not intended and they should not change frequently, such a model is fine.


Summary


Model development is an important part of not only QML programming, but programming as a whole. As Fred Brooks said: “Show the flowcharts, hide the tables and I will be puzzled, show me your tables and, most likely, I will not need flowcharts, they will be obvious”. That data is a central topic in programming. Designing data structures and accessing them is a crucial task and largely determines the architecture of the program.


Knowing the tools we covered in this and the previous section will help you organize your data in the most appropriate way, and then around the data and the program itself. Since the concept of Model-View is one of the fundamental ones in QML, these tools are enough.


I looked at various ways to create models. From my own experience I can say that the most used are QAbstractItemModel, ListModel, and JavaScript arrays. So I recommend them to pay attention first of all.


')

Source: https://habr.com/ru/post/302428/


All Articles