
Qt: Writing a generic model for QML ListView
To some, the material of this article may seem too simple, to some useless, but I’m sure that for beginners in Qt and QML who are faced with the need to create models for the first time ListView
, this will be useful at least as an alternative *, fast and quite effective solution with point of view "price / quality".
* At a minimum, at the time, I didn’t manage to google anything like that. If you know and can add - welcome.
What is the noise about?
With the advent of QML in Qt, creating user interfaces has become easier and faster ... until close interaction with C ++ code is required. The creation of exported C ++ classes is described quite well in the documentation and as long as you work with simple structures everything is really quite trivial (well, almost). The main nuisance arises when you need to "show" the elements of a container in QML, and in a simple way - collections, and especially when these elements have a complex structure of nested objects, or even other collections.
Interesting?
It is assumed that you are familiar with the Qt terminology and such words as a 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 ListView
. Following the Qt documentation, there are several ways to pass data into it, but there is only one option suitable for C ++ implementation - this is creating your own model through inheritance from QAbstractItemModel
. How to do it on a habr there were already articles, for example this . And everything would be fine, but let's first outline some facts:
- As a container in Qt more often than we use
QList
, - To avoid unnecessary copying, we usually place objects on the heap.
- And to simplify memory management, we use some kind of smart pointers. For Qt, "native" works quite well
QSharedPointer
. - Container elements themselves frequently
QObject
s because we need properties exported to QML (for this, you can still use Q_GADGET if you do not need to change these properties, but there are also some “jokes” there). - We often don’t have many elements, say, up to a million (for example, some news feed, or a list of files in a regular directory (npm_modules don’t count :)) If there are much more elements that need to be displayed, then you probably already need First of all, to solve more serious problems - with UX.
Having implemented several such models, you quickly realize that the amount of boilerplate in them is off scale. Just forwarding role names is worth it. Anyway, why, if all of 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: have a sheet interface and at the same time be able to somehow act as a model for ListView
, for example ...ListModel
Why is the sheet generally a model?
The sheet creates delegates (renderers for individual elements) not all at once, but only those that should be visible at the moment, plus an optional cache. When scrolling through a sheet, delegates that have gone beyond the boundaries of visibility are destroyed, and new ones are created. Now let's add a new item to our list. In this case, it ListView
must be informed which index has been added and if this index is between the indexes that are currently displayed, then you need to create a new delegate, initialize it with data and place it between existing ones. When deleting the situation is the opposite. When we change the properties of elements, signals are added here to change the "roles" - the data that is visible directly in the delegate (to be honest, I don’t know who came up with this name).
If we use "pure" C ++ structures, then we have no choice: the only way to somehow export such data is through our own descendant 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, it becomes very inconvenient and inappropriate. And if you need to transfer also the structure through the role, then the task becomes even more complicated, because in this case, the structure is transferred placed in QVariant
with all the consequences.
Passing a structural element in QML
First, let's take a look, but how can we pass a container element with a complex structure to the delegate?
Suppose we have a list of elements with this 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 an heir from QAbstractListModel
(which in turn is an heir from QAbstractItemModel
). We take the popular one as storage QList
. But do not assign any roles! Instead, we proceed as follows:
- Register our classes in QMLEngine:
qmlRegisterUncreatableType( "Personal", 1, 0, "Person", "interface" );
qmlRegisterUncreatableType( "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 this QObject
. You can argue for a long time about the effectiveness of this approach, but in real-life tasks, saving on QObject often turns out to be saving on matches and is incomparable with labor costs. And if you look at the general tendency 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 it’s just an “interface”.
- We implement in the Q_INVOKABLE model a method that will return a pointer to our structure at its index.
Total, it turns out something like this:
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 personal;
}
Now, with such a model, in view, we can substitute a typed object into it and then use the delegate to instantiate it ! Moreover, Qt Creator is quite capable at prompting to prompt the fields of this structure, which, in turn, cannot but rejoice.
// P.S. Не забыть этот класс тоже зарегистрировать в QMLEngine
Personal {
id: personalModel
}
ListView {
model: personalModel
delegate: Item {
// index - стандартная доступная роль. но нужно помнить, что доступна она только здесь
property Employee employee: personalModel.getEmployee(index)
Text {
text: employee.person.name
}
}
}
Step One: Index Model
Now let's analyze what we got. But it turned out that we QAbstractListModel
use only indexes from ours , the rest of the work is done by Q_OBJECT
s and their meta-properties. Those. we can implement in general a model that will work only with indices and this will be enough to ListView
know what is happening! We get the following 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, for example 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. But what if ... and generalize the container?
Step two: add the container
Now it’s not a shame to write a template class for the container. You can get confused 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 one, in my case it is . as the container most often used in Qt, and - to worry less about ownership. (PS Something you still need to worry about, but more on that later)QList
QList
QSharedPointer
Well, let's go. Ideally, we would like our model to have the same interface as QList
it would mimic it to the maximum, but to forward everything would be too inefficient, because in reality we need not so much: only those methods that are used to change are append, insert, removeAt. For the rest, you can simply make the public accessor to the inner sheet "as is."
template
class ListModelImplTemplate : public IndicesListModelImpl {
public:
void append( const QSharedPointer& item ) {
storage.append( item );
IndicesListModelImpl::push();
}
void append( const QList>& 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& item ) {
storage.insert( i, item );
IndicesListModelImpl::insertAt( i );
}
// --- QList-style comfort ;) ---
ListModelImplTemplate& operator+=( const QSharedPointer& t ) {
append( t );
return *this;
}
ListModelImplTemplate& operator<<( const QSharedPointer& t ) {
append( t );
return *this;
}
ListModelImplTemplate& operator+=( const QList>& list ) {
append( list );
return *this;
}
ListModelImplTemplate& operator<<( const QList>& 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>& list() const {
return storage;
}
int count() const {
return storage.count();
}
protected:
QList> storage;
};
Step Three: getItem () Method and Model Generalization
It would seem that it remains to make one more template from this class and then use it as a type for any collection and deal with the end, for example like this:
class Personal : public QObject {
public:
ListModel* personal;
}
But there is a problem and the third step is not in vain here: QObject classes that use the Q_OBJECT macro cannot be template and the first time you try to compile such a MOC class you will be happy to tell you about it. Have you come?
There is still a solution to this problem, albeit not so elegant: the good old macro #define! We will generate our class dynamically ourselves, where necessary (in every way better than writing a boilerplate every time). Fortunately, we only have to implement one method!
#define DECLARE_LIST_MODEL( NAME, ITEM_TYPE )
class NAME : ListModelImplTemplate {
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* )
Separately, it is worth talking about QQmlEngine::setObjectOwnership( obj, QQmlEngine::CppOwnership );
- this thing is needed in order for QMLEngine not to dare to take up the management of our facilities. If we want to use our object in some kind of JS function and place it in a variable with a local scope, then the JS Engine will crash it without hesitation when exiting this function, because ours does QObject
not have a parent. On the other hand, we do not use parent intentionally, because we already have control over the lifetime of the object with the help QSharedPointer
and we don’t need another mechanism.
Total, we get the following picture:
- Базовую реализацию
QAbstractListModel
—IndicesListModelImpl
— для манипуляции с индексами, чтобыListView
реагировал - Честный шаблонный класс-обертку над стандартным контейнером, задача которого обеспечивать редактирование этого контейнера и вызов методов вышестоящего
IndicesListModelImpl
- Сгенерированный класс — наследник всего этого "добра", который предоставляет единственный метод для доступа к элементам из QML.

Заключение
Пользоваться полученным решением очень просто: там где нам необходимо экспортировать в QML некоторую коллекцию объектов, тут же создаем нужную модель и тут же ее используем. Например, у нас имеется некоторый класс-провайдер (а в терминологии Qt — Backend), одно из свойств которого должно предоставлять список неких 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( new DataItem );
}
Q_SIGNALS:
void changed();
private:
ListModel_DataItem m_itemsModel;
};
And of course, with all 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.