Back Multiple Qt Views with One QAbstractItemModel
Stop copying contact data into every Qt view. QAbstractItemModel lets QTableView, QListView, and QTreeView share one data source.
23 Aug 2026, 01:39 UTC

One contact list, three views, and no single source of truth
You open a Qt desktop application and see the same contact list in a sidebar, a main table, and a search panel. Each view holds its own copy of the data. When a user edits a phone number in the table, the sidebar still shows the old number until you restart the app. The bug is not in the views; it is in the architecture.
The useful takeaway: let Qt’s model/view architecture own the data once, and let QTableView, QListView, and QTreeView simply project it. A subclass of QAbstractItemModel acts as the single source of truth, so every view reflects the same contacts without duplicating rows in memory.
This assumes Qt 6. The core QAbstractItemModel contract carries over from Qt 5, but module placement for some convenience classes has changed.
Why views fall out of sync
When each widget stores its own QList<Contact> or std::vector, you must push updates to every copy. Miss one, and the UI lies to the user. A model owns data and exposes it through an interface; a view renders that data and handles interaction; a delegate controls how items are painted and edited.
What QAbstractItemModel actually separates
QAbstractItemModel does not store data itself. It defines how views ask for data. Views call model methods using QModelIndex objects, which are lightweight coordinates into the model. The model answers with QVariant values tagged by roles: Qt::DisplayRole for what the user sees, Qt::EditRole for what the user can change.
To make a concrete model, override at least five members:
- rowCount and columnCount tell the view how large the grid is.
- index converts a row and column into a QModelIndex.
- parent returns the index’s parent (invalid for flat lists).
- data returns the value for an index and role.
Structural changes such as inserting rows require beginInsertRows and endInsertRows so views can update their selection and scroll state. Skipping them can leave views out of sync or crash.
Worked example: a read-only contact table model
This example exposes a list of contacts as a single-column table. It is read-only, so it does not implement setData or editing flags.
File: src/contactmodel.h
#ifndef CONTACTMODEL_H
#define CONTACTMODEL_H
#include <QAbstractItemModel>
#include <QList>
#include <QString>
struct Contact {
QString name;
QString email;
};
class ContactModel : public QAbstractItemModel {
Q_OBJECT
public:
explicit ContactModel(QObject *parent = nullptr);
void setContacts(const QList<Contact> &contacts);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
private:
QList<Contact> m_contacts;
};
#endif // CONTACTMODEL_H
File: src/contactmodel.cpp
#include "contactmodel.h"
ContactModel::ContactModel(QObject *parent)
: QAbstractItemModel(parent) {}
void ContactModel::setContacts(const QList<Contact> &contacts) {
beginResetModel();
m_contacts = contacts;
endResetModel();
}
int ContactModel::rowCount(const QModelIndex &parent) const {
if (parent.isValid())
return 0;
return m_contacts.size();
}
int ContactModel::columnCount(const QModelIndex &parent) const {
if (parent.isValid())
return 0;
return 1;
}
QModelIndex ContactModel::index(int row, int column, const QModelIndex &parent) const {
if (!hasIndex(row, column, parent))
return QModelIndex();
return createIndex(row, column);
}
QModelIndex ContactModel::parent(const QModelIndex &) const {
return QModelIndex();
}
QVariant ContactModel::data(const QModelIndex &index, int role) const {
if (!index.isValid() || index.row() >= m_contacts.size())
return QVariant();
const Contact &c = m_contacts[index.row()];
if (role == Qt::DisplayRole && index.column() == 0)
return c.name;
return QVariant();
}
Wire it into a window:
QTableView *table = new QTableView(this);
ContactModel *model = new ContactModel(this);
model->setContacts({{"Ada Lovelace", "[contact removed]"},
{"Grace Hopper", "[contact removed]"}});
table->setModel(model);
Where to run: build with qmake or CMake in a Qt 6 project, then run the executable from your project build directory as a normal user. No elevated permissions are required.
Expected check: a window opens showing one column with the two names. Attaching the same model instance to a QListView should show the identical rows in both views.
Risk: if you modify m_contacts directly without beginResetModel or beginInsertRows/endInsertRows, attached views will not refresh and may crash when they read stale indices.
Limitations and when to keep it simple
A custom model is more code than QStandardItemModel, which stores items for you. Use QStandardItemModel for small, static lists where you do not need custom logic. Use QAbstractItemModel when your data already exists elsewhere, such as a database or network cache, and you do not want to copy it into Qt’s item classes.
QAbstractItemModel is not thread-safe by default. GUI views must be updated from the GUI thread. If a background worker changes the data, post the update to the GUI thread before emitting model signals.
Verify it works
Create a minimal Qt 6 widgets project, add the two files above, and instantiate the model in your main window. Confirm QTableView displays the contact names. Then attach the same model to a QListView and confirm both views show identical rows. Check the Qt documentation for QAbstractItemModel::data and QAbstractItemModel::index signatures in your installed Qt version, because exact signatures can vary by patch release.
Start with one model
Start with one model and one view. Once the names appear, attach a second view to the same model. If both update together, you have removed the duplication that causes stale UI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.