c++qtrowonchangeqabstracttablemodel

QAbstractTableModel and emit dataChanged for a single row


I derived a model from QAbstractTableModel and now I want to notify, that the data of a whole row has been changed. If for example the data of a row with index 5 is changed (4 columns), than using the following code works as expected.

emit dataChanged(index(5,0), index(5, 0));
emit dataChanged(index(5,1), index(5, 1));
emit dataChanged(index(5,2), index(5, 2));
emit dataChanged(index(5,3), index(5, 3));

But if I try to achieve the same with only one emit, ALL columns of ALL rows in the view are updated.

emit dataChanged(index(5, 0), index(5, 3));

What I am doing wrong here?

Minimal example (C++11, QTCreator 4.7.1, Windows 10 (1803), 64 Bit)

demo.h

#pragma once
#include <QAbstractTableModel>
#include <QTime>
#include <QTimer>

class Demo : public QAbstractTableModel
{
  Q_OBJECT
  QTimer * t;
public:
  Demo()
  {
    t = new QTimer(this);
    t->setInterval(1000);
    connect(t, SIGNAL(timeout()) , this, SLOT(timerHit()));
    t->start();
  }

  QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
  {
    int c = index.column();
    if (role == Qt::DisplayRole)
    {
      QString strTime = QTime::currentTime().toString();
      if (c == 0) return "A" + strTime;
      if (c == 1) return "B" + strTime;
      if (c == 2) return "C" + strTime;
      if (c == 3) return "D" + strTime;
    }
    return QVariant();
  }

  int rowCount(const QModelIndex &) const override { return 10; }
  int columnCount(const QModelIndex &) const override { return 4; }
private slots:
  void timerHit()
  {
    //Works
    emit dataChanged(index(5,0), index(5, 0));
    emit dataChanged(index(5,1), index(5, 1));
    emit dataChanged(index(5,2), index(5, 2));
    emit dataChanged(index(5,3), index(5, 3));

    //emit dataChanged(index(5,0), index(5, 3)); // <-- Doesn't work
  }
};

main.cpp

#include "demo.h"
#include <QApplication>
#include <QTreeView>

int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  QTreeView dataView;
  Demo dataModel{};
  dataView.setModel( &dataModel );
  dataView.show();
  return a.exec();
}

Solution

  • I think the problem lies with certain assumptions you're making with regard the behaviour of QTreeView when the QAbstractItemModel::dataChanged signal is emitted.

    Specifically, you assume that the view will only invoke QAbstractItemModel::data on those indexes that are specified in the signal. That's not necessarily the case.

    Looking at the source for QAbstractItemView::dataChanged (Qt 5.11.2) you'll see...

    void QAbstractItemView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
    {
        Q_UNUSED(roles);
        // Single item changed
        Q_D(QAbstractItemView);
        if (topLeft == bottomRight && topLeft.isValid()) {
            const QEditorInfo &editorInfo = d->editorForIndex(topLeft);
            //we don't update the edit data if it is static
            if (!editorInfo.isStatic && editorInfo.widget) {
                QAbstractItemDelegate *delegate = d->delegateForIndex(topLeft);
                if (delegate) {
                    delegate->setEditorData(editorInfo.widget.data(), topLeft);
                }
            }
            if (isVisible() && !d->delayedPendingLayout) {
                // otherwise the items will be update later anyway
                update(topLeft);
            }
        } else {
            d->updateEditorData(topLeft, bottomRight);
            if (isVisible() && !d->delayedPendingLayout)
                d->viewport->update();
        }
    
    #ifndef QT_NO_ACCESSIBILITY
        if (QAccessible::isActive()) {
            QAccessibleTableModelChangeEvent accessibleEvent(this, QAccessibleTableModelChangeEvent::DataChanged);
            accessibleEvent.setFirstRow(topLeft.row());
            accessibleEvent.setFirstColumn(topLeft.column());
            accessibleEvent.setLastRow(bottomRight.row());
            accessibleEvent.setLastColumn(bottomRight.column());
            QAccessible::updateAccessibility(&accessibleEvent);
        }
    #endif
        d->updateGeometry();
    }
    

    The important point is that this code behaves differently depending on whether or not the signal specifies a single QModelIndex -- e.g. topLeft is the same as bottomRight. If they are the same then the view tries to ensure that only that model index is updated. However, if multiple model indexes are specified then it will invoke...

    d->viewport->update();
    

    which will, presumably, result in the data for all visible model indexes being queried.

    Since your implementation of Demo::data always returns new data based on the current time you will see the entire visible part of the view update giving the impression that the dataChanged signal was emitted for all rows and columns.

    So the fix is really to make your data model more ``stateful'' -- it needs to keep track of values rather than simply generating them on demand.