Model/View Without the Fog
I triggered a crash today by deleting the third row of a list. The list had only two visible rows, which was the first clue that my idea of the data and the view’s idea had stopped speaking to each other.
The old code filled a widget item by item and kept pointers to those items in the document. Qt 4’s model/view classes ask for a cleaner bargain: the model owns the shape and meaning of the data, while views ask questions through QModelIndex. That index is a coordinate plus model identity, not the object itself.
For a flat list, the smallest useful model is not large:
class ContactModel : public QAbstractListModel
{
public:
ContactModel(QObject *parent = 0)
: QAbstractListModel(parent)
{
}
int rowCount(const QModelIndex &parent = QModelIndex()) const
{
return parent.isValid() ? 0 : contacts.count();
}
QVariant data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.model() != this ||
index.row() < 0 || index.row() >= contacts.count() ||
index.column() != 0)
return QVariant();
if (role == Qt::DisplayRole)
return contacts.at(index.row()).name;
return QVariant();
}
private:
QList<Contact> contacts;
};
The parent check is important even for a list. A valid parent asks for children beneath an item; this model has none. Returning the full count there can make a view believe it has discovered a tree of infinite enthusiasm.
The more interesting part is changing data. The view cannot infer that a row appeared because a QList grew. The model must announce structural changes around the actual mutation:
void ContactModel::append(const Contact &contact)
{
int row = contacts.count();
beginInsertRows(QModelIndex(), row, row);
contacts.append(contact);
endInsertRows();
}
Those calls let attached views adjust selections, editors, and cached geometry coherently. For an ordinary value change, I modify the value and emit dataChanged(index, index) instead. Resetting the whole model after every edit appears easier, but it discards selection and makes modest updates expensive.
My crash came from removal code that changed the collection first and emitted signals afterwards. During that gap, a view requested an index using the old shape against the new data. Correct order is not decorative:
void ContactModel::remove(int row)
{
if (row < 0 || row >= contacts.count())
return;
beginRemoveRows(QModelIndex(), row, row);
contacts.removeAt(row);
endRemoveRows();
}
Selections add another wrinkle. A normal QModelIndex is intended for immediate use. If I must retain an index while rows can move, I use QPersistentModelIndex, and even then I ask whether storing a domain identifier would be clearer. Persistent indexes survive model notifications, not arbitrary lies told by the model.
Sorting and filtering belong in a proxy when possible. QSortFilterProxyModel sits between source and view, translating indexes in both directions. That means code receiving a selected proxy index must map it before addressing source data:
QModelIndex sourceIndex = proxy->mapToSource(view->currentIndex());
Contact contact = model->contactAt(sourceIndex.row());
Forgetting this works beautifully until sorting changes the order. These are the bugs that wait politely through a demonstration and introduce themselves to the first user.
I prefer models that expose domain data through roles and operations, with widgets knowing as little as possible about storage. One model can then drive a list, a combo box, and perhaps a custom view without three synchronisation routines. The caveat is that model/view has real ceremony. A fixed list of five labels does not need a heroic subclass; QStringListModel may be enough.
Editable models need one more honest contract. flags() must advertise editable indexes, and setData() must validate the role and value before changing storage. Returning true means the edit happened, not merely that the method received a pleasant visit:
bool ContactModel::setData(const QModelIndex &index,
const QVariant &value, int role)
{
if (role != Qt::EditRole || !index.isValid() ||
index.model() != this || index.row() < 0 ||
index.row() >= contacts.count() || index.column() != 0)
return false;
contacts[index.row()].name = value.toString();
emit dataChanged(index, index);
return true;
}
The model and bounds checks matter here too. An index from another model is not permission to address this model’s storage, even when its row happens to fit. The delegate asks the model to commit; it does not gain ownership of the underlying record.
There is also a testing advantage. I can exercise rowCount, data, insertion, and removal with QCoreApplication, no screen required. My current test records the emitted row ranges and checks the resulting names. It is not glamorous, but neither is debugging a stale selection at midnight.
Qt 4’s mechanism got less foggy when I stopped treating the model as a collection adapter. The view trusts its signals and invariants. Once I stopped lying to it, the disappearing third row quit crashing, though it remains philosophically ambitious.