Overview
Qt’s Model/View architecture separates data (model) from presentation (view), with a delegate controlling rendering. This avoids duplicating data and makes it easy to show the same data in multiple views.
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Model │ ←───► │ View │ ←───► │ Delegate │
│ (data) │ │ (display)│ │ (render) │
└─────────┘ └──────────┘ └──────────┘
Ready-Made Models
Qt ships several models you can use immediately:
| Model | Use Case |
|---|---|
QStringListModel | Simple list of strings |
QStandardItemModel | General-purpose table/tree |
QFileSystemModel | File system browsing |
QSqlTableModel | Database table |
#include <QStringListModel>
#include <QListView>
QStringList items = { "UART", "SPI", "I2C", "CAN", "USB" };
QStringListModel *model = new QStringListModel(this);
model->setStringList(items);
QListView *view = new QListView(this);
view->setModel(model);
Custom QAbstractListModel
For custom data, subclass QAbstractListModel:
// sensormodel.h
#include <QAbstractListModel>
#include <QVector>
struct SensorEntry {
QString name;
float value;
QString unit;
};
class SensorModel : public QAbstractListModel {
Q_OBJECT
public:
enum Roles {
NameRole = Qt::UserRole + 1,
ValueRole,
UnitRole
};
explicit SensorModel(QObject *parent = nullptr);
// Required overrides
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; // for QML
// Mutation
void addSensor(const SensorEntry &entry);
void updateValue(int row, float value);
private:
QVector<SensorEntry> m_sensors;
};
// sensormodel.cpp
#include "sensormodel.h"
SensorModel::SensorModel(QObject *parent) : QAbstractListModel(parent) {}
int SensorModel::rowCount(const QModelIndex &parent) const {
if (parent.isValid()) return 0;
return m_sensors.size();
}
QVariant SensorModel::data(const QModelIndex &index, int role) const {
if (!index.isValid() || index.row() >= m_sensors.size())
return {};
const auto &s = m_sensors.at(index.row());
switch (role) {
case Qt::DisplayRole:
case NameRole: return s.name;
case ValueRole: return s.value;
case UnitRole: return s.unit;
}
return {};
}
QHash<int, QByteArray> SensorModel::roleNames() const {
return {
{ NameRole, "name" },
{ ValueRole, "value" },
{ UnitRole, "unit" },
};
}
void SensorModel::addSensor(const SensorEntry &entry) {
beginInsertRows(QModelIndex(), m_sensors.size(), m_sensors.size());
m_sensors.append(entry);
endInsertRows();
}
void SensorModel::updateValue(int row, float value) {
if (row < 0 || row >= m_sensors.size()) return;
m_sensors[row].value = value;
QModelIndex idx = index(row);
emit dataChanged(idx, idx, { ValueRole });
}
Custom QAbstractTableModel
For tabular data (rows + columns):
class LogModel : public QAbstractTableModel {
Q_OBJECT
public:
int rowCount(const QModelIndex &parent = {}) const override;
int columnCount(const QModelIndex &parent = {}) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
void appendLog(const QString &level, const QString &msg, const QDateTime &ts);
private:
struct LogEntry { QDateTime timestamp; QString level; QString message; };
QVector<LogEntry> m_logs;
};
QVariant LogModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (role != Qt::DisplayRole || orientation != Qt::Horizontal) return {};
switch (section) {
case 0: return "Timestamp";
case 1: return "Level";
case 2: return "Message";
}
return {};
}
Views
QListView
QListView *listView = new QListView(this);
listView->setModel(sensorModel);
listView->setSelectionMode(QAbstractItemView::SingleSelection);
connect(listView->selectionModel(), &QItemSelectionModel::currentChanged,
[this](const QModelIndex ¤t) {
qDebug() << "Selected:" << current.data(SensorModel::NameRole);
});
QTableView
QTableView *tableView = new QTableView(this);
tableView->setModel(logModel);
tableView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
tableView->verticalHeader()->setVisible(false);
tableView->setAlternatingRowColors(true);
tableView->setSortingEnabled(true);
QTreeView
QTreeView *treeView = new QTreeView(this);
treeView->setModel(new QFileSystemModel(this));
static_cast<QFileSystemModel*>(treeView->model())->setRootPath("/");
treeView->setRootIndex(
static_cast<QFileSystemModel*>(treeView->model())->index("/home")
);
Custom Delegate — Custom Rendering
#include <QStyledItemDelegate>
#include <QPainter>
class SensorDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override
{
painter->save();
// Background
QColor bg = (option.state & QStyle::State_Selected)
? QColor("#89b4fa") : QColor("#1e1e2e");
painter->fillRect(option.rect, bg);
// Name
painter->setPen(QColor("#cdd6f4"));
QString name = index.data(SensorModel::NameRole).toString();
painter->drawText(option.rect.adjusted(12, 0, 0, 0),
Qt::AlignVCenter | Qt::AlignLeft, name);
// Value
painter->setPen(QColor("#a6e3a1"));
QString val = QString::number(index.data(SensorModel::ValueRole).toFloat(), 'f', 2)
+ " " + index.data(SensorModel::UnitRole).toString();
painter->drawText(option.rect.adjusted(0, 0, -12, 0),
Qt::AlignVCenter | Qt::AlignRight, val);
painter->restore();
}
QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override {
return QSize(0, 48);
}
};
listView->setItemDelegate(new SensorDelegate(listView));
Proxy Models — Filtering & Sorting
#include <QSortFilterProxyModel>
SensorModel *source = new SensorModel(this);
QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(source);
proxy->setFilterRole(SensorModel::NameRole);
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
QListView *view = new QListView(this);
view->setModel(proxy);
// Filter by search text
QLineEdit *search = new QLineEdit(this);
connect(search, &QLineEdit::textChanged,
proxy, &QSortFilterProxyModel::setFilterFixedString);
Summary
| Class | Role |
|---|---|
QAbstractListModel | Custom 1D list data |
QAbstractTableModel | Custom rows + columns |
QStringListModel | Simple string list |
QStandardItemModel | Flexible drag/drop tree/table |
QListView | 1D list display |
QTableView | 2D table display |
QTreeView | Hierarchical tree display |
QStyledItemDelegate | Custom row rendering |
QSortFilterProxyModel | Sort and filter without touching the model |
Advanced: Drag-and-Drop Model
class DeviceListModel : public QAbstractListModel {
Q_OBJECT
public:
Qt::ItemFlags flags(const QModelIndex &index) const override {
Qt::ItemFlags f = QAbstractListModel::flags(index);
return f | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
}
Qt::DropActions supportedDropActions() const override {
return Qt::MoveAction;
}
bool dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int col, const QModelIndex &parent) override
{
Q_UNUSED(col); Q_UNUSED(parent);
if (!data->hasFormat("application/x-device-id")) return false;
QString id = QString::fromUtf8(data->data("application/x-device-id"));
insertRow(row < 0 ? rowCount() : row);
setData(index(row < 0 ? rowCount()-1 : row), id);
return true;
}
};
QDataWidgetMapper — Model ↔ Form
#include <QDataWidgetMapper>
QStandardItemModel *model = new QStandardItemModel(10, 3, this);
// ... fill model with device data
QDataWidgetMapper *mapper = new QDataWidgetMapper(this);
mapper->setModel(model);
mapper->addMapping(m_nameEdit, 0); // column 0 → QLineEdit
mapper->addMapping(m_baudSpin, 1); // column 1 → QSpinBox
mapper->addMapping(m_statusLabel, 2); // column 2 → QLabel
// Navigate rows
connect(prevBtn, &QPushButton::clicked, mapper, &QDataWidgetMapper::toPrevious);
connect(nextBtn, &QPushButton::clicked, mapper, &QDataWidgetMapper::toNext);
mapper->toFirst();
Visual: Model/View Roles
┌──────────────────────────────────────────┐
│ QModelIndex │
│ row=2, col=1, parent=QModelIndex() │
└──────────────┬───────────────────────────┘
│ model->data(index, role)
▼
┌────────────────────────────────────────────────────────┐
│ Qt::DisplayRole → "36.5 °C" (shown in view) │
│ Qt::EditRole → 36.5 (edited value) │
│ Qt::DecorationRole → QIcon(...) (icon) │
│ Qt::ToolTipRole → "Last read 5s ago" │
│ Qt::UserRole + 1 → custom data (e.g. sensor id) │
└────────────────────────────────────────────────────────┘
Lab — Editable Sensor Table
class SensorTableModel : public QAbstractTableModel {
Q_OBJECT
struct Row { QString name; float value; QString unit; bool active; };
QVector<Row> m_data;
public:
SensorTableModel(QObject *p=nullptr) : QAbstractTableModel(p) {
m_data = {{"Temp", 36.5, "°C", true},
{"Volt", 3.3, "V", false},
{"Pres", 1013, "hPa",true}};
}
int rowCount(const QModelIndex&) const override { return m_data.size(); }
int columnCount(const QModelIndex&) const override { return 4; }
QVariant headerData(int section, Qt::Orientation o, int role) const override {
if (role != Qt::DisplayRole) return {};
if (o == Qt::Horizontal)
return QStringList{"Name","Value","Unit","Active"}[section];
return section + 1;
}
QVariant data(const QModelIndex &idx, int role) const override {
if (!idx.isValid()) return {};
const auto &r = m_data[idx.row()];
if (role == Qt::DisplayRole || role == Qt::EditRole) {
switch(idx.column()) {
case 0: return r.name;
case 1: return r.value;
case 2: return r.unit;
case 3: return r.active;
}
}
if (role == Qt::CheckStateRole && idx.column() == 3)
return r.active ? Qt::Checked : Qt::Unchecked;
return {};
}
bool setData(const QModelIndex &idx, const QVariant &val, int role) override {
if (!idx.isValid()) return false;
auto &r = m_data[idx.row()];
if (role == Qt::EditRole) {
switch(idx.column()) {
case 0: r.name = val.toString(); break;
case 1: r.value = val.toFloat(); break;
case 2: r.unit = val.toString(); break;
}
}
if (role == Qt::CheckStateRole && idx.column() == 3)
r.active = (val.toInt() == Qt::Checked);
emit dataChanged(idx, idx, {role});
return true;
}
Qt::ItemFlags flags(const QModelIndex &idx) const override {
Qt::ItemFlags f = QAbstractTableModel::flags(idx);
if (idx.column() == 3) return f | Qt::ItemIsUserCheckable;
return f | Qt::ItemIsEditable;
}
};
// Usage
SensorTableModel *model = new SensorTableModel(this);
QTableView *view = new QTableView(this);
view->setModel(model);
view->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
view->setAlternatingRowColors(true);
Interview Questions
Q1: What is the difference between QAbstractItemModel and QStandardItemModel?
QStandardItemModelis a ready-made generic model that storesQStandardItemobjects — easy to use but not optimal for large datasets.QAbstractItemModelis an interface you subclass to wrap your own data directly — better performance and control.
Q2: Why use roles in a model?
Roles allow the same model to serve different purposes to different consumers.
DisplayRoleformats data as a string for display;EditRolereturns the raw value for editing; customUserRole+nvalues carry domain-specific data (e.g., sensor ID for filtering).
Q3: What is a proxy model and when should you use one?
A proxy model wraps another model and transforms its data without modifying the source.
QSortFilterProxyModeladds sorting and filtering. Custom proxies can merge models or restructure data. The source model stays unchanged.
Q4: How do you invalidate a model correctly?
Wrap bulk data changes in
beginResetModel()/endResetModel(). For row inserts/removes, usebeginInsertRows()/endInsertRows(). For cell updates, emitdataChanged()with the affected range. Using the correct notifications keeps views synchronized.
Q5: How does QDataWidgetMapper help with forms?
It binds a row in a model directly to form widgets, handling the read/write mapping automatically. Navigate rows with
toNext()/toPrevious().submit()writes widget values back to the model. Great for master-detail UIs.
Application: Live Device Log Table
class LogModel : public QAbstractTableModel {
Q_OBJECT
struct LogEntry { QDateTime ts; QString level; QString msg; };
QVector<LogEntry> m_entries;
static const int MAX = 500;
public:
int rowCount(const QModelIndex&) const override { return m_entries.size(); }
int columnCount(const QModelIndex&) const override { return 3; }
QVariant headerData(int s, Qt::Orientation o, int r) const override {
if (r != Qt::DisplayRole || o != Qt::Horizontal) return {};
return QStringList{"Timestamp","Level","Message"}[s];
}
QVariant data(const QModelIndex &i, int r) const override {
if (!i.isValid() || r != Qt::DisplayRole) return {};
const auto &e = m_entries[i.row()];
switch(i.column()) {
case 0: return e.ts.toString("hh:mm:ss.zzz");
case 1: return e.level;
case 2: return e.msg;
}
return {};
}
QVariant data(const QModelIndex &i, int r) const override {
// Color coding
if (r == Qt::ForegroundRole) {
const auto &e = m_entries[i.row()];
if (e.level == "ERROR") return QColor("#f38ba8");
if (e.level == "WARN") return QColor("#fab387");
return QColor("#a6e3a1");
}
return {};
}
public slots:
void append(const QString &level, const QString &msg) {
if (m_entries.size() >= MAX) {
beginRemoveRows({}, 0, 0);
m_entries.removeFirst();
endRemoveRows();
}
beginInsertRows({}, m_entries.size(), m_entries.size());
m_entries.append({QDateTime::currentDateTime(), level, msg});
endInsertRows();
}
};
// Usage
LogModel *log = new LogModel(this);
QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(log);
proxy->setFilterKeyColumn(1); // filter by level
QTableView *view = new QTableView(this);
view->setModel(proxy);
view->setAlternatingRowColors(true);
view->verticalHeader()->setVisible(false);
view->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
// Filter controls
QComboBox *filterBox = new QComboBox;
filterBox->addItems({"All","INFO","WARN","ERROR"});
connect(filterBox, &QComboBox::currentTextChanged, [proxy](const QString &t){
proxy->setFilterFixedString(t == "All" ? "" : t);
});
References
| Resource | Link |
|---|---|
| Qt Docs: Model/View | https://doc.qt.io/qt-6/model-view-programming.html |
| Qt Docs: QAbstractItemModel | https://doc.qt.io/qt-6/qabstractitemmodel.html |
| Qt Docs: QSortFilterProxyModel | https://doc.qt.io/qt-6/qsortfilterproxymodel.html |
| Qt Docs: QDataWidgetMapper | https://doc.qt.io/qt-6/qdatawidgetmapper.html |
Next tutorial → Qt Networking