Model/View Architecture

QAbstractItemModel, QListView, QTableView, and custom models for data-driven UIs.

8 min read
70737 chars

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:

ModelUse Case
QStringListModelSimple list of strings
QStandardItemModelGeneral-purpose table/tree
QFileSystemModelFile system browsing
QSqlTableModelDatabase 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 &current) {
            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

ClassRole
QAbstractListModelCustom 1D list data
QAbstractTableModelCustom rows + columns
QStringListModelSimple string list
QStandardItemModelFlexible drag/drop tree/table
QListView1D list display
QTableView2D table display
QTreeViewHierarchical tree display
QStyledItemDelegateCustom row rendering
QSortFilterProxyModelSort 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?

QStandardItemModel is a ready-made generic model that stores QStandardItem objects — easy to use but not optimal for large datasets. QAbstractItemModel is 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. DisplayRole formats data as a string for display; EditRole returns the raw value for editing; custom UserRole+n values 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. QSortFilterProxyModel adds 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, use beginInsertRows() / endInsertRows(). For cell updates, emit dataChanged() 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

ResourceLink
Qt Docs: Model/Viewhttps://doc.qt.io/qt-6/model-view-programming.html
Qt Docs: QAbstractItemModelhttps://doc.qt.io/qt-6/qabstractitemmodel.html
Qt Docs: QSortFilterProxyModelhttps://doc.qt.io/qt-6/qsortfilterproxymodel.html
Qt Docs: QDataWidgetMapperhttps://doc.qt.io/qt-6/qdatawidgetmapper.html

Next tutorial → Qt Networking