Remove rows from QAbstractListModel

Programmer picture Programmer · Oct 10, 2013 · Viewed 12.6k times · Source

I have a custom model which derives from QAbstractListModel which is exposed to QML. I need to support operations to add new items and remove existing items. While insertion operation works without any problems, removal operation causes the application to crash while calling endRemoveRows() function.

    void GPageModel::addNewPage()
    {
        if(m_pageList.count()<9)
        {
            beginInsertRows(QModelIndex(),rowCount(),rowCount());
            GPage * page = new GPage();
            QQmlEngine::setObjectOwnership(page,QQmlEngine::CppOwnership);
            page->setParent(this);
            page->setNumber(m_pageList.count());
            page->setName("Page " + QString::number(m_pageList.count()+1));
            m_pageList.append(page);
            endInsertRows();
        }
    }

    void GPageModel::removePage(const int index)
    {
        if(index>=0 && index<m_pageList.count())
        {        
            beginRemoveRows(QModelIndex(),index,index);
            qDebug()<<QString("beginRemoveRows(QModelIndex(),%1,%1)").arg(index);
            GPage * page = m_pageList.at(index);        
            m_pageList.removeAt(index);
            delete page;
            endRemoveRows();
        }
    }

The class GPage derives from QObject. I am struck trying to figure out what is causing the app to crash while trying to call endRemoveRows(). I get "ASSERT failure in QList::at: "index out of range"" when endRemoveRows() is called.How do I remove the rows from a QAbstracListModel? Is there any other way?

I am using Qt 5.1.0 on a Windows 7 64 bit machine.

Answer

The code below works fine for me. Your problem is probably elsewhere. This is for Qt 5 due to use of Qt Quick Controls.

There are two views accessing the same model, this visually confirms that the model emits proper signals to inform the views of the changes. The page additions and removals are done via the standard insertRows and removeRows methods, exported through Q_INVOKABLE. There's no need for any custom methods on this model, so far. The Q_INVOKABLE is a workaround for some missing functionality for the interface between QML and QAbstractItemModel.

screenshot

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickWindow>
#include <QAbstractListModel>
#include <QQmlContext>
#include <QtQml>

class GPage : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString name NOTIFY nameChanged MEMBER m_name)
    Q_PROPERTY(int number NOTIFY numberChanged MEMBER m_number)
    QString m_name;
    int m_number;
public:
    GPage(QObject * parent = 0) : QObject(parent), m_number(0) {}
    GPage(QString name, int number, QObject * parent = 0) :
        QObject(parent), m_name(name), m_number(number) {}
    Q_SIGNAL void nameChanged(const QString &);
    Q_SIGNAL void numberChanged(int);
};

class PageModel : public QAbstractListModel {
    Q_OBJECT
    QList<GPage*> m_pageList;
public:
    PageModel(QObject * parent = 0) : QAbstractListModel(parent) {}
    ~PageModel() { qDeleteAll(m_pageList); }
    int rowCount(const QModelIndex &) const Q_DECL_OVERRIDE {
        return m_pageList.count();
    }
    QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE {
        if (role == Qt::DisplayRole || role == Qt::EditRole) {
            return QVariant::fromValue<QObject*>(m_pageList.at(index.row()));
        }
        return QVariant();
    }
    bool setData(const QModelIndex &index, const QVariant &value, int role) Q_DECL_OVERRIDE {
        Q_UNUSED(role);
        GPage* page = value.value<GPage*>();
        if (!page) return false;
        if (page == m_pageList.at(index.row())) return true;
        delete m_pageList.at(index.row());
        m_pageList[index.row()] = page;
        QVector<int> roles;
        roles << role;
        emit dataChanged(index, index, roles);
        return true;
    }
    Q_INVOKABLE bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) Q_DECL_OVERRIDE {
        Q_UNUSED(parent);
        beginInsertRows(QModelIndex(), row, row + count - 1);
        for (int i = row; i < row + count; ++ i) {
            QString const name = QString("Page %1").arg(i + 1);
            GPage * page = new GPage(name, i + 1, this);
            m_pageList.insert(i, page);
            QQmlEngine::setObjectOwnership(page, QQmlEngine::CppOwnership);
        }
        endInsertRows();
        return true;
    }
    Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) Q_DECL_OVERRIDE {
        Q_UNUSED(parent);
        beginRemoveRows(QModelIndex(), row, row + count - 1);
        while (count--) delete m_pageList.takeAt(row);
        endRemoveRows();
        return true;
    }
};

int main(int argc, char *argv[])
{
    PageModel model1;
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    model1.insertRows(0, 1);
    engine.rootContext()->setContextProperty("model1", &model1);
    qmlRegisterType<GPage>();
    engine.load(QUrl("qrc:/main.qml"));
    QObject *topLevel = engine.rootObjects().value(0);
    QQuickWindow *window = qobject_cast<QQuickWindow *>(topLevel);
    window->show();
    return app.exec();
}

#include "main.moc"

main.qml

import QtQuick 2.0
import QtQml.Models 2.1
import QtQuick.Controls 1.0

ApplicationWindow {
    width: 300; height: 300
    Row {
        width: parent.width
        anchors.top: parent.top
        anchors.bottom: column.top
        Component {
            id: commonDelegate
            Rectangle {
                width: view.width
                implicitHeight: editor.implicitHeight + 10
                color: "transparent"
                border.color: "red"
                border.width: 2
                radius: 5
                TextInput {
                    id: editor
                    anchors.margins: 1.5 * parent.border.width
                    anchors.fill: parent
                    text: edit.name // "edit" role of the model, to break the binding loop
                    onTextChanged: {
                        display.name = text;
                        model.display = display
                    }
                }
            }
        }
        ListView {
            id: view
            width: parent.width / 2
            height: parent.height
            model: DelegateModel {
                id: delegateModel1
                model: model1
                delegate: commonDelegate
            }
            spacing: 2
        }
        ListView {
            width: parent.width / 2
            height: parent.height
            model: DelegateModel {
                model: model1
                delegate: commonDelegate
            }
            spacing: 2
        }
    }
    Column {
        id: column;
        anchors.bottom: parent.bottom
        Row {
            Button {
                text: "Add Page";
                onClicked: model1.insertRows(delegateModel1.count, 1)
            }
            Button {
                text: "Remove Page";
                onClicked: model1.removeRows(pageNo.value - 1, 1)
            }
            SpinBox {
                id: pageNo
                minimumValue: 1
                maximumValue: delegateModel1.count;
            }
        }
    }
}

main.qrc

<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
    </qresource>
</RCC>