Some people will find the material in this article too simple, useless to someone, but I’m sure that for Qt and QML beginners who are faced with the need to create models for ListView
for the first time, this will be useful as an alternative * quick and fairly effective solution. in terms of "price / quality".
* At least, at one time I didn’t manage anything like that. If you know and can add - welcome.
With the advent of QML in Qt, creating user interfaces has become easier and faster ... until you have to work closely with C ++ code. Creating exported C ++ classes is fairly well described in the documentation and as long as you work with simple structures, everything is really quite trivial (well, almost). The main trouble comes when you need to "show" in QML the elements of a container, and in a simple way - the collection, and especially when these elements have a complex structure of nested objects, and even other collections.
It is assumed that you are familiar with the terminology of Qt and words such as delegate , role , container as applied to lists and list components will not surprise you, as I once did ...
The most used component for displaying QML list data is the ListView
. Following the Qt documentation, there are several ways to pass data to it, but the only option that is suitable for a C ++ implementation is to create your model through inheritance from QAbstractItemModel
. How to do this in Habré article already, for example, this . And everything would be fine, but let's start with some facts:
QList
,QSharedPointer
works quite well.QObject
, since we need properties exported to QML (for this, you can still use Q_GADGET, if you don’t need to change these properties, but there you also have your own "jokes").Having realized several such models, you quickly realize that the amount of boilerplate in them exceeds the limit. Just throwing the names of roles is worth something. And in general, why, if all this is already in Q_OBJECT
and Q_GADGET
? It quickly comes to mind that I would like to have some kind of template container that could generalize all this: to have a sheet interface and at the same time be able to act as a model for ListView
, for example ListModel<ModelItem> itemsCollection
...
The sheet creates delegates (renderers for individual items) not all at once, but only those that should be visible at the moment plus an optional cache. When you scroll the sheet, delegates that went beyond the scope of visibility are destroyed, and new ones are created. Now let's add a new item to our list. In this case, the ListView
should be informed which index was added and if this index is between the indexes that are currently displayed, it means you need to create a new delegate, initialize it with data and place it between the existing ones. When deleting, the situation is reversed. When we change the properties of the elements, there are added signals about the change of "roles" - the data that can be seen directly in the delegate (frankly, I do not know who thought of it so to call it).
If we use "pure" C ++ structures, then we have no choice: the only way to export such data is our own successor model from QAbstractItemModel. And if we have Q_OBJECT or Q_GADGET elements, then they already know how to "show" their properties in QML and additional duplication of roles, as well as "juggling" the model when changing such objects becomes very inconvenient and impractical. And if you need to pass through the role and the structure, the task becomes even more complicated, because in this case, the structure is transferred hosted in QVariant
with all the consequences.
First, let's take a look, how can you delegate a container element with a complex structure to the delegate?
Suppose we have a list of elements with the following structure of objects:
class Person + string name + string address + string phoneNumber class Employee + Person* person + string position + double salary
Of course, in this case, to display such a structure, it could be painlessly made flat, but let's imagine that the data is complex and we cannot do that.
So, create a successor from QAbstractListModel
(which in turn is a successor from QAbstractItemModel
). As storage we take popular QList
. But do not ask any roles! Instead, we proceed as follows:
qmlRegisterUncreatableType<Person>( "Personal", 1, 0, "Person", "interface" ); qmlRegisterUncreatableType<Employee>( "Personal", 1, 0, "Employee", "interface" );
and do not forget yet
Q_DECLARE_METATYPE( Person* ) Q_DECLARE_METATYPE( Employee* )
In this case, I assume that our classes are QObject
. One can argue about the effectiveness of such an approach for a long time, but in real problems saving on a QObject often turns out to be a saving on matches and is incommensurable with labor costs. And if you look at the tendencies at all to write applications on Electron ...
Why uncreatable - because it's easier. We are not going to create these objects in QML, which means we do not need a default constructor, for example. For us, this is just an "interface".
Total, something turns out such:
class Personal : public QAbstractListModel { public: // Q_INVOKABLE Employee* getEmployee( int index ); // QAbstractListModel: int rowCount( const QModelIndex& parent ) const override { return personal.count(); } // , .. . QVariant data( const QModelIndex& index, int role ) const override { return QVariant(); } // - // QList, // beginInsertRows(), endInsertRows() . // , , . private: QList<Employee*> personal; }
Now, with such a model, in view we can substitute into it and then use a typed object when instantiating a delegate! Moreover, Qt Creator is quite capable when typing to podkazyvat fields of this structure, which in turn also can not but rejoice.
// PS QMLEngine Personal { id: personalModel } ListView { model: personalModel delegate: Item { // index - . , property Employee employee: personalModel.getEmployee(index) Text { text: employee.person.name } } }
Now let's analyze what we did. And it turned out that we only use indexes from our QAbstractListModel
, the rest of the work is done by Q_OBJECT
and their meta-properties. Those. we can implement, by and large, a model that will only work with indexes and that will be enough for ListView
know what is happening! We get this interface:
class IndicesListModelImpl : public QAbstractListModel { Q_OBJECT Q_PROPERTY( int count READ count NOTIFY countChanged ) public: int count() const; // --- QAbstractListModel --- int rowCount( const QModelIndex& parent ) const override; QVariant data( const QModelIndex& index, int role ) const override; protected: // Create "count" indices and push them to end void push( int count = 1 ); // Remove "count" indices from the end. void pop( int count = 1 ); // Remove indices at particular place. void removeAt( int index, int count = 1 ); // Insert indices at particular place. void insertAt( int index, int count = 1 ); // Reset model with new indices count void reset( int length = 0 ); Q_SIGNALS: void countChanged( const int& count ); private: int m_count = 0; };
where in the implementation we simply inform the view that certain indices seem to have changed, like this:
void IndicesListModelImpl::insertAt( int index, int count ) { if ( index < 0 || index > m_length + 1 || count < 1 ) return; int start = index; int end = index + count - 1; beginInsertRows( QModelIndex(), start, end ); m_count += count; endInsertRows(); emit countChanged( m_count ); }
Well, not bad, now we can inherit not directly from QAbstractListModel
, but from our improvised class, where there is already half of the logic we need. What if ... and a container to summarize?
Now it’s not a shame to write a template class for a container. You can mess up and make two parameters for the template: the container and the stored type, thus allowing the use of anything at all, but I would not and stopped at the most frequently used, in my case it is QList<QSharedPointer<ItemType>>
. QList
as the most frequently used container in Qt, and QSharedPointer
to worry less about ownership. (PS Something that still needs to be worried, but more on that later)
Well, let's go. Ideally, we want our model to have the same interface as QList
and thus mimic him to the maximum, but it would be too inefficient to forward everything, because really we need not so much: only those methods that are used to change - append, insert , removeAt. For the rest, you can simply make a public accessor to the internal list "as is".
template <class ItemType> class ListModelImplTemplate : public IndicesListModelImpl { public: void append( const QSharedPointer<ItemType>& item ) { storage.append( item ); IndicesListModelImpl::push(); } void append( const QList<QSharedPointer<ItemType>>& list ) { storage.append( list ); IndicesListModelImpl::push( list.count() ); } void removeAt( int i ) { if ( i > length() ) return; storage.removeAt( i ); IndicesListModelImpl::removeAt( i ); } void insert( int i, const QSharedPointer<ItemType>& item ) { storage.insert( i, item ); IndicesListModelImpl::insertAt( i ); } // --- QList-style comfort ;) --- ListModelImplTemplate& operator+=( const QSharedPointer<ItemType>& t ) { append( t ); return *this; } ListModelImplTemplate& operator<<( const QSharedPointer<ItemType>& t ) { append( t ); return *this; } ListModelImplTemplate& operator+=( const QList<QSharedPointer<ItemType>>& list ) { append( list ); return *this; } ListModelImplTemplate& operator<<( const QList<QSharedPointer<ItemType>>& list ) { append( list ); return *this; } // Internal QList storage accessor. It is restricted to change it directly, // since we need to proxy all this calls, but it is possible to use it's // iterators and other useful public interfaces. const QList<QSharedPointer<ItemType>>& list() const { return storage; } int count() const { return storage.count(); } protected: QList<QSharedPointer<ItemType>> storage; };
It would seem that it remains to make another template from this class and then use it as a type for any collection and deal with it, for example:
class Personal : public QObject { public: ListModel<Employee>* personal; }
But there is a problem and the third step is not in vain: QObject classes that use the Q_OBJECT macro cannot be template, and at the very first attempt to compile such a MOC class, you are happy to tell about it. All sailed?
Not at all, there is a solution to this problem, though not so elegant: the good old macro #define! We will generate our class dynamically ourselves, where it is needed (any better than writing a boilerplate every time). Fortunately, it remains for us to implement only one method!
#define DECLARE_LIST_MODEL( NAME, ITEM_TYPE ) class NAME : ListModelImplTemplate<ITEM_TYPE> { Q_OBJECT protected: Q_INVOKABLE ITEM_TYPE* item( int i, bool keepOwnership = true ) const { if ( i >= 0 && i < storage.length() && storage.length() > 0 ) { auto obj = storage[i].data(); if ( keepOwnership ) QQmlEngine::setObjectOwnership( obj, QQmlEngine::CppOwnership ); return obj; } else { return Q_NULLPTR; } } }; Q_DECLARE_METATYPE( NAME* )
We should also tell about QQmlEngine::setObjectOwnership( obj, QQmlEngine::CppOwnership );
- This thing is needed for so that QMLEngine did not even think to manage our facilities. If we want to use our object in some JS function and place it in a variable with a local scope, then the JS Engine will crash it when it exits this function, because our QObject
no parent. On the other hand, parent, we do not use intentionally, because we already have object lifetime control using QSharedPointer
and we do not need another mechanism.
Total, we receive such picture:
QAbstractListModel
- IndicesListModelImpl
- for manipulating indexes so that ListView
respondsIndicesListModelImpl
It’s very simple to use this solution: where we need to export some collection of objects to QML, we immediately create the necessary model and use it immediately. For example, we have some class provider (and in Qt terminology, Backend), one of whose properties should provide a list of some DataItem
:
// DECLARE_LIST_MODEL( ListModel_DataItem, DataItem ) class Provider : public QObject { Q_OBJECT Q_PROPERTY( ListModel_DataItem* itemsModel READ itemsModel NOTIFY changed ) public: explicit Provider( QObject* parent = Q_NULLPTR ); ListModel_DataItem* itemsModel() { return &m_itemsModel; }; Q_INVOKABLE void addItem() { m_itemsModel << QSharedPointer<DataItem>( new DataItem ); } Q_SIGNALS: void changed(); private: ListModel_DataItem m_itemsModel; };
And of course, with all of this together: with the template and the full code of the example of use, you can take and familiarize yourself with the github .
Any additions, comments and pull requests are welcome.
Source: https://habr.com/ru/post/349822/
All Articles