Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

How to delete row from TableView properly



  • I have a table I created with TableView in qml.

    I can delete a row with right click on the Id field like this:

    26ef30f3-b7bd-4a36-ba99-2878044a9f42-image.png

    The deletion etc works but I get these errors in qml:

    52ce968c-c472-43ea-9998-be023b617333-image.png

    The method for delete rows looks like this in main.qml:

    function deleteRowFromDatabase(row) {
           console.log("before" + model.countOfRows())
    
           if (!model.removeEntry(row)) {
               console.log(qsTr("remove row %1 failed").arg(row))
           }
    
           model = QuestionsProxyModel
           console.log("after" + model.countOfRows())
       }
    

    The error point to the delegate row of id in main.qml

            DelegateChoice {
                column: 0
                delegate: QuestionIdDelegate {
                    id: questionIdDelegate
                    width: tableView.columnWidthProvider(column)
                    text: model.id                    /// this is undefined
                    row: model.row
    
                    Component.onCompleted: {
                        questionIdDelegate.markForDelete.connect(
                                    tableView.deleteRowFromDatabase)
                    }
                }
            }
    

    Removing of the rows is implemented from C++ In a class derrived from QIdentityProxyModel in questionsproxmodel.h:

    bool QuestionsProxyModel::removeEntry(int row)
    {
        return removeRows(row, 1);
    }
    

    This model takes a class QuestionSqlTableModel derrived from QSqlTableModel as a source model

    The remove rows is implemented like this in questionssqltablemodel.qml:

         bool QuestionSqlTableModel::removeRows(int row, int count,
                                                   const QModelIndex &parent)
            {
                auto result = QSqlTableModel::removeRows(row, count, parent);
                if (result) {
                    select(); // row is not deleted from sql database until select is called
                }
            
                return result;
            }
    

    From my understanding the countOfRows of the model gets updated only after the select() is called so I assume between QSqlTableModel::removeRows and select the TableView reads one more time with the non existing row and causes these errors in QML. How can that be prevented?

    Full Source code to try it out:

    main.cpp:

    #include <QDebug>
    #include <QGuiApplication>
    #include <QQmlApplicationEngine>
    #include <QQuickStyle>
    
    #include <QFile>
    #include <QSqlDatabase>
    #include <QSqlQuery>
    
    #include <QSqlError>
    
    #include "questionsproxymodel.h"
    #include "questionsqltablemodel.h"
    
    int main(int argc, char *argv[])
    {
        QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    
        QGuiApplication app(argc, argv);
    
        QUrl dbUrl{"file:///home/sandro/Desktop/test.db"};
        auto exists = QFile::exists(dbUrl.toLocalFile());
    
        auto db = QSqlDatabase::addDatabase("QSQLITE", "DBConnection");
        db.setDatabaseName(dbUrl.toLocalFile());
        db.open();
    
        if (!exists) {
            const QString questionTableName = "questions";
            QSqlQuery query{db};
            query.exec("CREATE TABLE " + questionTableName +
                       " ("
                       "id INTEGER PRIMARY KEY AUTOINCREMENT)");
        }
    
        QScopedPointer<QuestionSqlTableModel> questionSqlTableModel(
            new QuestionSqlTableModel(nullptr, db));
    
        QScopedPointer<QuestionsProxyModel> questionsProxyModel{
            new QuestionsProxyModel};
        questionsProxyModel->setSourceModel(questionSqlTableModel.get());
    
        if (!exists) {
            for (int i = 0; i < 10; ++i) {
                questionsProxyModel->addNewEntry();
            }
        }
    
        QQmlApplicationEngine engine;
    
        qmlRegisterSingletonInstance<QuestionsProxyModel>(
            "QuestionsProxyModels", 1, 0, "QuestionsProxyModel",
            questionsProxyModel.get());
    
        const QUrl url(QStringLiteral("qrc:/qml/main.qml"));
        engine.load(url);
    
        return app.exec();
    }
    

    questionssqltablemodel.h

    #include <QSqlTableModel>
    
    class QuestionSqlTableModel : public QSqlTableModel {
        Q_OBJECT
    public:
        explicit QuestionSqlTableModel(QObject *parent = nullptr,
                                       const QSqlDatabase &db = QSqlDatabase());
    
        bool removeRows(int row, int count, const QModelIndex &parent) override;
    };
    

    questionssqltablemodel.cpp

    #include <QBuffer>
    #include <QDebug>
    #include <QPixmap>
    
    #include <QSqlError>
    #include <QSqlField>
    #include <QSqlRecord>
    #include <QSqlRelationalDelegate>
    
    QuestionSqlTableModel::QuestionSqlTableModel(QObject *parent,
                                                 const QSqlDatabase &db)
        : QSqlTableModel{parent, db}
    {
        setTable("questions");
        setSort(0, Qt::AscendingOrder);
        if (!select()) {
            qDebug() << "QuestionSqlTableModel: Select table questions failed";
        }
        setEditStrategy(EditStrategy::OnFieldChange);
    }
    
    bool QuestionSqlTableModel::removeRows(int row, int count,
                                           const QModelIndex &parent)
    {
        auto result = QSqlTableModel::removeRows(row, count, parent);
        if (result) {
            select(); // row is not deleted from sql database until select is called
        }
    
        return result;
    }
    

    questionsproxymodel.h:

    #include <QIdentityProxyModel>
    #include <QObject>
    
    class QuestionsProxyModel : public QIdentityProxyModel {
        Q_OBJECT
    
        enum questionRoles {
            idRole = Qt::UserRole + 1,
        };
    
    public:
        QuestionsProxyModel(QObject *parent = nullptr);
    
        QHash<int, QByteArray> roleNames() const override;
    
        Q_INVOKABLE QVariant data(const QModelIndex &index,
                                  int role = Qt::DisplayRole) const override;
    
        bool setData(const QModelIndex &index, const QVariant &value,
                     int role = Qt::EditRole) override;
    
        bool addNewEntry();
        Q_INVOKABLE bool removeEntry(int row);
        Q_INVOKABLE int countOfRows() const;
    
    private:
        QModelIndex mapIndex(const QModelIndex &source, int role) const;
    };
    

    questionsproxymodel.h:

    #include <QBuffer>
    #include <QDebug>
    #include <QPixmap>
    
    #include <QByteArray>
    
    #include <QSqlError>
    #include <QSqlTableModel>
    
    QuestionsProxyModel::QuestionsProxyModel(QObject *parent)
        : QIdentityProxyModel(parent)
    {
    }
    
    QHash<int, QByteArray> QuestionsProxyModel::roleNames() const
    {
        QHash<int, QByteArray> roles;
        roles[idRole] = "id";
        return roles;
    }
    
    QVariant QuestionsProxyModel::data(const QModelIndex &index, int role) const
    {
        QModelIndex newIndex = mapIndex(index, role);
        if (role == idRole) {
    
            return QIdentityProxyModel::data(newIndex, Qt::DisplayRole);
        }
        return QIdentityProxyModel::data(newIndex, role);
    }
    
    bool QuestionsProxyModel::setData(const QModelIndex &index,
                                      const QVariant &value, int role)
    {
        QModelIndex newIndex = mapIndex(index, role);
    
        if (role == idRole) {
    
            return QIdentityProxyModel::setData(newIndex, value, Qt::EditRole);
        }
        return QIdentityProxyModel::setData(newIndex, value, role);
    }
    
    bool QuestionsProxyModel::addNewEntry()
    {
        auto newRow = rowCount();
    
        if (!insertRows(newRow, 1)) {
            return false;
        }
        if (!setData(createIndex(newRow, 0), newRow + 1)) {
            removeRows(newRow, 1);
            return false;
        }
        auto sqlModel = qobject_cast<QSqlTableModel *>(sourceModel());
        return sqlModel->submit();
    }
    
    bool QuestionsProxyModel::removeEntry(int row)
    {
        return removeRows(row, 1);
    }
    
    int QuestionsProxyModel::countOfRows() const
    {
        return rowCount();
    }
    
    QModelIndex QuestionsProxyModel::mapIndex(const QModelIndex &source,
                                              int role) const
    {
        switch (role) {
        case idRole:
            return createIndex(source.row(), 0);
        }
        return source;
    }
    

    main.qml

    import QtQuick 2.15
    import QtQuick.Controls 2.15
    import QtQuick.Window 2.15
    import Qt.labs.qmlmodels 1.0
    import QtQuick.Controls.Material 2.15
    
    import QuestionsProxyModels 1.0
    
    ApplicationWindow {
        id: root
        visible: true
        width: 1460
        height: 800
    
        TableView {
            id: tableView
            width: parent.width
    
            anchors.fill: parent
            boundsBehavior: Flickable.StopAtBounds
    
            reuseItems: true
            clip: true
            property var columnWidths: [60]
            columnWidthProvider: function (column) {
                return columnWidths[column]
            }
    
            model: QuestionsProxyModel
    
            delegate: DelegateChooser {
                id: chooser
    
                DelegateChoice {
                    column: 0
                    delegate: QuestionIdDelegate {
                        id: questionIdDelegate
                        width: tableView.columnWidthProvider(column)
                        text: model.id
                        row: model.row
    
                        Component.onCompleted: {
                            questionIdDelegate.markForDelete.connect(
                                        tableView.deleteRowFromDatabase)
                        }
                    }
                }
            }
            ScrollBar.vertical: ScrollBar {}
    
            function deleteRowFromDatabase(row) {
                console.log("before" + model.countOfRows())
    
                if (!model.removeEntry(row)) {
                    console.log(qsTr("remove row %1 failed").arg(row))
                }
    
                model = QuestionsProxyModel
                console.log("after" + model.countOfRows())
            }
        }
    }
    

    QuestionIdDelegate.qml:

    import QtQuick 2.15
    import QtQuick.Controls 2.15
    
    TextField {
        property int row
    
        signal markForDelete(int row)
    
        id: root
    
        implicitHeight: 100
    
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
    
        readOnly: true
    
        background: Frame {}
    
        MouseArea {
            id: mouseArea
            anchors.fill: parent
            acceptedButtons: Qt.RightButton
    
            onClicked: {
                eraseContextMenu.popup(root, 0, mouseArea.mouseY + 10)
            }
        }
    
        Menu {
            id: eraseContextMenu
            y: root.y
            MenuItem {
                text: qsTr("Delete entry")
                onTriggered: {
                    eraseDialog.open()
                    eraseContextMenu.close()
                }
            }
            MenuItem {
                text: qsTr("Cancel")
                onTriggered: {
                    eraseContextMenu.close()
                }
            }
        }
    
        Dialog {
            id: eraseDialog
            title: qsTr("Delete database entry")
            modal: true
            focus: true
    
            contentItem: Label {
                id: label
                text: qsTr("Do you really want to erase the entry with id %1?").arg(
                          root.text)
            }
    
            onAccepted: {
                markForDelete(root.row)
            }
    
            standardButtons: Dialog.Ok | Dialog.Cancel
        }
    }
    


  • @sandro4912
    I couldn't see any call to

    beginRemoveRows(...)
    ...
    endRemoveRows(...)
    

    in your code. Refer to:
    https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows

    Sometimes it is easier just to use the following pair instead:

    beginResetModel();
    ...
    endResetModel();
    

    https://doc.qt.io/qt-5/qabstractitemmodel.html#beginResetModel



  • @Diracsbracket said in How to delete row from TableView properly:

    endResetModel();

    Were would you add that?

    I added it to the function:

    bool QuestionSqlTableModel::removeRows(int row, int count,
                                           const QModelIndex &parent)
    

    but still get the errors in qml.



  • @sandro4912

    Normally you would place it like this:

    bool QuestionSqlTableModel::removeRows(int row, int count,
                                           const QModelIndex &parent)
    {
        beginRemoveRows() //<--- or beginResetModel()
    
        auto result = QSqlTableModel::removeRows(row, count, parent);
        if (result) {
            select(); // row is not deleted from sql database until select is called
        }
    
       endRemoveRows(); //<-- or endResetModel()
       return result;
    }
    


  • You are right I totally forgot to add that.

    Although It did not solve the assign errors. I still had to change in the delegates:

    text: model.id === undefined ? "" : model.id
    

    And I realized there is another error which still shows up:

    If I delete the row 1 and then the row 2 my output looks like this:
    enter image description here

    This binding loop error points Dialog in QuestionsIDDelegate:

    Dialog {
        id: eraseDialog
        title: qsTr("Delete database entry")
        modal: true
        focus: true
    
        contentItem: Label {
            id: label
            text: qsTr("Do you really want to erase the entry with id %1?").arg(
                      root.text)
        }
    
        onAccepted: {
            markForDelete(root.row)
        }
    
        standardButtons: Dialog.Ok | Dialog.Cancel
    }


  • @sandro4912 said in How to delete row from TableView properly:

    This binding loop error points Dialog in QuestionsIDDelegate:

    That is probably a bug in your Qt version. I'm tested it in Qt 5.15.2, and I get no binding loop.
    As a workaround, try setting the height of the Label explicitly.

    @sandro4912 said in How to delete row from TableView properly:

    text: model.id === undefined ? "" : model.id

    Assuming that id is always defined in your model, this shouldn't be necessary (imagine having to do this check for every field of your model you use in the delegate).

    Maybe try with begin/endResetModel() instead of begin/endRemoveRows() ?



  • @Diracsbracket said in How to delete row from TableView properly:

    Qt 5.15

    Did you try to reproduce the binding loop to delete several elements? If I delete the first it did not happen.

    Im Also on Qt 5.15



  • I also asked this question here:

    Even If I used signals and slots to invoke the methods not directly from javascript I still get these errors:

    https://stackoverflow.com/questions/65328173/how-to-delete-row-from-tableview-properly/65382947?noredirect=1#comment115712152_65382947


Log in to reply