Custom QAbstractTableModel class updating QTableView



  • Hi all,

    I already read a few threads on this forum dealing with similar issues but sadly they didn't provide "the" solution.

    I built my own QAbstractTableModel class which fills a QTableView with data from a .csv file. The file is used for logging purposes and stores its (new) data every 10 minutes. The data is shown inside a QTableView but needs to be updated manually (button or closing and reopening the QDialog). What I want to achieve is that the view is automatically updated whenever new data is written to the .csv file.

    The code snippets of my TableModel looks as follows:

    int QCsvTableModel::rowCount(const QModelIndex &parent) const
    {
        Q_UNUSED(parent);
        return csvMatrix.rowCount();
    }
    
    int QCsvTableModel::columnCount(const QModelIndex &parent) const
    {
        Q_UNUSED(parent);
        return csvMatrix.columnCount();
    }
    
    QVariant QCsvTableModel::data(const QModelIndex &index, int role) const
    {
        if (index.isValid())
            if (role == Qt::DisplayRole || role == Qt::EditRole)
                return csvMatrix.at(index.row(), index.column());
        return QVariant();
    }
    
    bool QCsvTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
    {
        if (index.isValid() && role == Qt::EditRole) {
            csvMatrix.setValue(index.row(), index.column(), value.toString());
    
            emit dataChanged(index,index);     // No difference if dataChanged is emitted or not
    
            return true;
        }
    
        return false;
    }
    

    MainWindow source:

    MainWindow::MainWindow(QWidget *parent) :
        QMainWindow(parent),
        ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
    
        QString fileName = ":/value.csv";
    
        if (!fileName.isEmpty()) {
            QCsvTableModel *model = new QCsvTableModel(this);
    
            QString extension = QFileInfo(QFile(fileName)).completeSuffix();
    
            if (extension.toLower() == "csv")     // known file extension
                model->loadFromFile(fileName);
    
            ui->tableView->setModel(model);
        } // if fileName ..
    
        connect(ui->tableView->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
                       this, SLOT(onModelsDataChanged(const QModelIndex&, const QModelIndex&)));
    }
    
    void MainWindow::onModelsDataChanged(const  QModelIndex  &topLeft,  const  QModelIndex  &bottomRight)
    {
        Q_UNUSED(topLeft);
        Q_UNUSED(bottomRight);
    
        qDebug() << "The data has changed." << endl;
    }
    

    But the dataChanged() signal isn't fired at all and "The data has changed." never shows up.

    What am I missing? Any help is appreciated!

    Seb


  • Moderators

    @Sebbo
    how does your loadFromFile() method look like?

    The dataChanged() signal is only emitted if the data for an existing index changed and needs update. You won't receive it for the loadFromFile() call, instead a model reset should happen there.



  • Hi raven-worx,

    thanks for your reply!!
    The loadFromFile() method looks as follows:

    bool QCsvTableModel::loadFromFile(const QString &fileName, const QChar &delim)
    {
        csvMatrix.clear();
        QChar delimiter;
        QFile file(fileName);
    
        if (delim == 0) {
            QString extension = QFileInfo(file).completeSuffix();
            if (extension.toLower() == "csv")
                delimiter = QChar(';');
        }
        else if (delim == QChar('"'))
                return false; // The only invalid delimiter is double quote (")
        else
            delimiter = delim;
        if (!file.isOpen())
            if (!file.open(QFile::ReadOnly|QFile::Text))
                return false;
    
        QString temp;
        QChar lastCharacter;
        QTextStream in(&file);
        QList<QString> row;
    
        while (true) {
            QChar character;
            in >> character;
            if (in.atEnd()) {
                if (lastCharacter == delimiter) // Cases where last character is equal to the delimiter
                    temp = "";
                checkString(temp, row, csvMatrix, delimiter, QChar('\n'));
                break;
            } else if (character == delimiter || character == QChar('\n'))
                checkString(temp, row, csvMatrix, delimiter, character);
            else {
                temp.append(character);
                lastCharacter = character;
            }
        }
    
        /*
        int row1 = csvMatrix.rowCount();
        QModelIndex index;
        beginInsertRows(QModelIndex(), row1, row1 + index.row() - 1);
        QModelIndex transposedIndex = createIndex(index.column(), index.row());
        emit dataChanged(transposedIndex, transposedIndex);
        emit layoutChanged();
        endInsertRows();
        */
    
        file.close();
        in.flush();
        in.reset();
    
        return true;
    }
    

    As you can see I already tried to emit the dataChanged signal from there but nothing happened.


  • Moderators

    @Sebbo
    First please make sure that the connect() call returns true. If not check the console output for the cause.

    Also no line of this code makes any (functional) sense:

    int row1 = csvMatrix.rowCount();
    QModelIndex index;
    beginInsertRows(QModelIndex(), row1, row1 + index.row() - 1);
    QModelIndex transposedIndex = createIndex(index.column(), index.row());
    emit dataChanged(transposedIndex, transposedIndex);
    emit layoutChanged();
    endInsertRows();
    

    As i said the correct approach for the loadFromFile() method would be to call beginRestModel() at the beginning of the method and endRestModel() at the end of the method. No dataChanged(), no beginInsertRows(), no layoutChanged(), etc. signals ...

    The dataChanged() signal should only called for already existing indexes.



  • @raven-worx

    I forgot to tell that I tested the method with beginResetModel() and endResetModel() as well (without trying to emit all the signals) which didn't do the trick. My apologies.

    The connect() call returns true.


  • Moderators

    @Sebbo
    but to make sure i didn't mean that the beginResetModel()/endResetModel() signals do internally emit the dataChanged() singal! Instead they emit modelAboutToBeReset() and modelReset() respectively.

    Whats do you actually want to achieve in the onModelsDataChanged() slot?



  • @raven-worx
    Thank you for the effort!!

    For testing purposes I've added a pushbutton which adds a row with some foo-text to the csv file. But since I wasn't able to show the newly stored data inside the qtableview I wanted to see whether the dataChanged() signal is emitted and calls the method with just a console output.
    What I've done in first place was to repaint() / update() the tableview inside the onModelsDataChanged() method.


  • Moderators

    @Sebbo
    ok here are my thoughts:

    The simplest approach is the "static" one. It just displays the contents of the csv file at the time it is opened:

    • in the loadFromFile() method do like i said and use the reset mechanism; the reset tells the view that it should forget all about what it knows from the model and that it should get all the data again

    The disadvantage of the rest-method is that the view looses it's selection, scroll position, current index, etc.

    For a more "dynamic" approach you need to do this:

    1. implement the same loadFromFile() method like for the "static" approach. But additionally you also install an QFileSystemWatcher on that file and connect it's fileChanged() signal to a new slot called something like "csvFileContentsChanged"
    2. in the loadFromFile() method you need to uninstall the old QFileSystemWatcher everytime a new/different file is set
    3. also save the count of lineNumbers read from the csv file
    4. in the new csvFileContentsChanged slot read the csv file line by line and compare the new line count to the previously stored one. If the new line count is higher you need to call beginInsertRows()/endInsertRows() signals, if the new count is smaller you need to call beginRemoveRows()/endRemoveRows(). And dataChanged() for the rest of the indexes which are already there.

    Pseudo code:

    void csvFileContentsChanged()
    {
         // read cvs file again into a local "tmpCsvMatrix" temporary variable
    
         int newLineCount = ...;
         int diff = qAbs( newLineCount - oldLineCount );
    
        if( newLineCount < oldLineCount )
        {
                beginRemoveRows( QModelindex(), newLineCount, newLineCount + diff - 1 );
                    csvMatrix = tmpCsvmatrix;
                endRemoveRows();
        }
        else if( newLineCount > oldLineCount )
        {
               beginInsertRows( QModelindex(), oldLineCount, oldLineCount + diff - 1 );
                    csvMatrix = tmpCsvMatrix;
                endInsertRows();
        }
    
        // emit dataChanged for the rest of the indexes, since we do not know if their content actually has changed inside the csv file
        if( rowCount() > 0 && columnsCount() > 0 )
              emit dataChanged( index(0, columnCount()-1), index(newLineCount-1 ,columnCount()-1) );
    }
    

    I haven't tested this though.



  • @raven-worx

    Thank you so much for your efforts and thoughts!!!
    I like the idea of your 2nd dynamic approach but I have to admit that the 1st one still doesn't work for me.

    beginResetModel();
    csvMatrix.clear();
    endResetModel();
    

    doesn't update the QTableView neither if new data is added outside Qt nor if the QPushButton is clicked which opens the file, writes into it (QTextStream) and closes it afterwards. :(


  • Moderators

    @Sebbo
    because is said you should trigger these signals at the beginning and the end of the loadFromFile() method. Mens the very first and very last call inside the method.
    Between those 2 signal calls you need to update your data structure.



  • @raven-worx

    That is exactly what I'm not getting right now. Don't get me wrong I know where to put the begin/endReset members of the model but I'm embarrassingly lost on how to update the data in between....


  • Moderators

    @Sebbo
    in between these 2 signals you just need to replace your csvMatrixvariable, thats all
    Since you take all the model data from this variable



  • @raven-worx
    Jackpot! Thank you so much for your help!! Finally I did it using QFileSystemWatcher.
    Thumbs up for you (if possible?!). :)

    Thread marked as solved.
    Cheers



Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.