Search code examples
c++qtqmlqabstractitemmodelqabstractlistmodel

QML Crash after Deleting Items from QAbstractListModel


I am running Qt 5.1.1 and QtCreator 2.8.1 on a Mac with OS-X 10.8.5. I have a QAbstractListModel that manages ImageData objects. I can load the images and display them just fine in QML using a GridView after registering an ImageProvider in main.cpp.

Next I select individual images in the view, e.g., several selected images are shown below with an orange border:

enter image description here

and then the C++ model function: deleteSelected(), produces the expected result:

enter image description here

However, when I attempt to resize the window, by say grabbing one of the corners, I get a crash. The stack trace says: Exception Type: EXC_CRASH (SIGABRT) and I get the Qt error:

ASSERT failure in QList<T>::at: "index out of range", … QtCore/qlist.h, line 452
The program has unexpectedly finished.

So perhaps I removed the model items improperly or failed to inform the model of the changes, but I thought that the begin and end RemoveRows emitted the proper signals to handle the synchronization? No doubt I am missing something else on this.

I have also called begin and end ResetModel, which prevents the application from crashing after resizing, but in that case, any other views attached to the model revert to showing all of the original items.

I have searched for a solution to this, tried lots of code experiments, and have studied code posted here, here, here, as well as several other places.

Can't seem to get this to work properly, Any suggestions? Thanks!

Below is some relevant code:


main.cpp:

...

// Other Classes:
#include "datamodelcontroller.h"
#include "imageprovider.h"


int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QQmlApplicationEngine engine;

    // Initialize and register model:
    DataModelController model;
    QQmlContext *context = engine.rootContext();
    context->setContextProperty("DataModelFromContext", &model);

    // Register image provider for each "role" to the model:
    ImageProvider *imageProvider = new ImageProvider(&model);
    engine.addImageProvider(QLatin1String("provider"), imageProvider);

    // Get the main.qml path from a relative path:
    PathResolver pathObject("qml/DebugProject/main.qml");
    QString qmlPath = pathObject.pathResult;

    // Create Component:
    QQmlComponent *component = new QQmlComponent(&engine);
    component->loadUrl(QUrl(qmlPath));

    // Display Window:
    QObject *topLevel = component->create();
    QQuickWindow *window = qobject_cast<QQuickWindow*>(topLevel);
    QSurfaceFormat surfaceFormat = window->requestedFormat();
    window->setFormat(surfaceFormat);
    window->show();

    return app.exec();
}

DataModelController.h:

class DataModelController : public QAbstractListModel
{
    Q_OBJECT
public:
    explicit DataModelController(QObject *parent = 0);

    enum DataRoles {
            FileNameRole = Qt::UserRole + 1,
            ImageRole
        };

        // QAbstractListModel:
        void addData(ImageData*& imageObj);
        int rowCount(const QModelIndex & parent = QModelIndex()) const;
        QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
        QHash<int, QByteArray> roleNames() const;

        // Get the Model Data:
        QList<ImageData*> getModelData();

signals:
    void imageSelectedStateChange(bool selectedImageStateValue);

public slots:
    void testLoadData();
    void removeData(int index);
    void setSelected(int index);
    bool isSelected(int index);
    void toggleSelected(int index);
    void deleteSelected();
    int count();

private:
    // Model Data here:
    QList<ImageData*> _modelData;

};

DataModelController.cpp

DataModelController::DataModelController(QObject *parent) : QAbstractListModel(parent)
{
}

void DataModelController::addData(ImageData*& imageObj) {
    beginInsertRows(QModelIndex(), rowCount(), rowCount());
    _modelData << imageObj; // for QList<>
    endInsertRows();
}

int DataModelController::rowCount(const QModelIndex &) const {
    return _modelData.size();
}

// Slot:
int DataModelController::count() {
    return this->rowCount();
}

QVariant DataModelController::data(const QModelIndex & index, int role) const
{
    if(!index.isValid())
        return QVariant();
    ImageData* imgObj = _modelData[index.row()];
    if (role == FileNameRole) {
        string imgFileName = imgObj->getFileName();
        QString fileName(imgFileName.c_str());
        return fileName;
    }
    if (role == ImageRole) {
        QString url = QString::number(index.row());
        return url;
    }
    return QVariant();
}

QHash<int, QByteArray> DataModelController::roleNames() const {
    QHash<int, QByteArray> roles;
    roles[FileNameRole] = "filename";
    roles[ImageRole] = "thumbnail";
    return roles;
}

QList<ImageData*> DataModelController::getModelData() {
    return _modelData;
}

void DataModelController::testLoadData()
{
    int width = 256, height = 256;
    for (int i = 0; i < 5; i++) {
        ostringstream digit;
        digit<<i;
        string imgPath("qml/DebugProject/TempImages/"+digit.str()+".jpg");
        PathResolver path(imgPath.c_str());
        QImage img(path.pathResult);

        // Initialize an Image Object:
        string fileName("file"+digit.str());
        ImageData *image = new ImageData(fileName);
        image->setData(NULL);
        image->nX = width;
        image->nY = height;
        image->setThumbnailQImage(img, width, height);
        image->setSelected(false);
        this->addData(image);
    }
}

void DataModelController::removeData(int index) {
    cout << "deleting index: " << index << endl;
    this->beginRemoveRows(QModelIndex(), index, index);
    _modelData.removeAt(index);
    // delete _modelData.takeAt(index); // tried this
    this->endRemoveRows();
    //this->beginResetModel();
    //this->endResetModel();
}

void DataModelController::setSelected(int index) {
    ImageData *imgObj = this->getModelData().at(index);
    if (!imgObj->getState()) {
        imgObj->setSelected(true);
        emit imageSelectedStateChange(imgObj->getState());
    }
}

bool DataModelController::isSelected(int index) {
    ImageData *imgObj = this->getModelData().at(index);
    return imgObj->getState();
}

void DataModelController::toggleSelected(int index) {
    ImageData *imgObj = this->getModelData().at(index);
    imgObj->setSelected(!imgObj->getState());
    emit imageSelectedStateChange(imgObj->getState());
}

void DataModelController::deleteSelected() {
    for (int i = this->rowCount()-1; i >= 0; i--) {
        if (this->isSelected(i)) {
            cout << i << ": state: " << this->isSelected(i) << endl;
            this->removeData(i);
            cout << this->rowCount();
        }
    }
}

ImageData.h:

class ImageData
{
public:
    ImageData();
    ImageData(string filename);

    long nX, nY; // image width, height

    void setFileName(string filename);
    string getFileName() const;

    void setSelected(bool state);
    bool getState() const;

    void setThumbnail(const int width, const int height);
    void setThumbnailQImage(QImage &imgStart, int width, int height);
    QImage getThumbnail() const;

    void setData(float* data);
    float* getData() const;


private:
    string _fileName;
    QImage _thumbnail;
    float* _data;
    bool _isSelected;

    void normalizeAndScaleData(float*& dataVector);
    void getMaxMinValues(float& datamax, float& datamin, float*& data, const int numPixels, bool verbose);
    unsigned char* getByteArrayFromFloatArray(int bytesPerRow, float*& dataVector);
};

main.qml:

import QtQuick 2.1
import QtQuick.Controls 1.0
import QtQuick.Window 2.1
import QtQuick.Controls.Styles 1.0
import QtQuick.Layouts 1.0

ApplicationWindow {
    id: mainAppWindow
    width: 1024*1.5*0.5
    height: 256
    color: "gray"
    property real imgScale: 0.5

    Window {
        id: gridViewMenu
        width: 160
        height: 128
        opacity: 0.8
        color: "black"
        visible: true
        x: 1024;
        Column {
            id: colButtons
            x: 10;
            y: 10;
            spacing: 25
            CustomButton {
                id: deleteSelectedButton; text: "Delete Selected"
                onClicked: {
                    DataModelFromContext.deleteSelected();
                }
            }
            CustomButton {
                id: testDataButton; text: "Load Test Images"
                visible: true
                onClicked: DataModelFromContext.testLoadData()
            }
        }
    }

    Rectangle {
        id: mainRect
        width: mainAppWindow.width*0.95
        height: mainAppWindow.height*0.8
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
        color: "transparent"

        // GRIDVIEW is here:
        Rectangle {
            id: gridContainer
            anchors.centerIn: parent
            width: parent.width
            height: parent.height*0.9
            color: "transparent"

            GridView {
                property int itemWidth: mainRect.width*imgScale;
                id: gridView
                interactive: true
                anchors.centerIn: parent
                width: parent.width;
                height: parent.height;
                cellWidth: itemWidth/3
                cellHeight: itemWidth/3
                focus: true
                model: DataModelFromContext
                delegate: gridDelegate
                Behavior on opacity {
                    NumberAnimation { duration: 500; easing.type: Easing.InOutQuad }
                }
                Keys.onPressed: {
                    if (event.key == Qt.Key_D) {
                        DataModelFromContext.deleteSelected()
                    }
                }
            } // end of gridView

            // GRIDVIEW Delegate:
            Component {
                id: gridDelegate
                Rectangle {
                    id: gridImageWrapper
                    width: gridView.cellWidth
                    height: gridView.cellHeight
                    color: "black"
                    Rectangle {
                        id: imageBorder
                        anchors.fill: parent
                        color: "transparent"
                        border.color: "green"
                        border.width: 1
                        z: 1
                    }
                    MouseArea {
                        id: selectImage
                        anchors.fill: parent
                        onClicked: {
                            // toggleSelected triggers the C++ signal: imageSelectedStateChange
                            DataModelFromContext.toggleSelected(index);
                            console.log(index + "; " + DataModelFromContext.count() )
                        }
                    }
                    Rectangle {
                        id: selectedImageBorder
                        anchors.fill: parent
                        color: "transparent"
                        border.color: "orange"
                        border.width: 2
                        opacity: 0
                        z: 2
                        Connections {
                            target: DataModelFromContext
                            onImageSelectedStateChange: {
                                selectedImageBorder.opacity = DataModelFromContext.isSelected(index);
                            }
                        }
                    }
                    Image {
                        property int itemWidth: mainRect.width*imgScale
                        id: frontIcon
                        anchors.centerIn: parent
                        source: "image://provider/" + thumbnail
                        smooth: true
                        visible: true
                        sourceSize.width: itemWidth/3;
                        sourceSize.height: itemWidth/3;
                    }

                } // end of Grid Delegate Rectangle

            } // end of Grid Delegate

        } // end of gridContainer Rectangle


    } // End: mainRect

} // End Main Application Window

ImageData.cpp:

#include "imagedata.h"

ImageData::ImageData()
{
}

ImageData::ImageData(string filename)
{
    _fileName = filename;
}

void ImageData::setFileName(string filename) {
    _fileName = filename;
}

string ImageData::getFileName() const {
    return _fileName;
}

void ImageData::setSelected(bool state) {
    _isSelected = state;
}

bool ImageData::getState() const {
    return _isSelected;
}

QImage ImageData::getThumbnail() const {
    return _thumbnail;
}

void ImageData::setThumbnailQImage(QImage &imgStart, int width, int height) {
    QImage img;
    //QImage *imgStart = new QImage(pix, nX, nY, bytesPerRow, QImage::Format_Indexed8);
    img = QPixmap::fromImage(imgStart).scaled(width, height, Qt::KeepAspectRatio).toImage();
    _thumbnail = img;
}

void ImageData::setData(float* data) {
    _data = data;
}

float* ImageData::getData() const {
    return _data;
}

// Function: getMaxMinValues
void ImageData::getMaxMinValues(float& datamax, float& datamin, float*& data, 
                                const int numPixels, bool verbose) {
    datamin  = 1.0E30;
    datamax  = -1.0E30;
    for (int pix = 0; pix < numPixels-1; pix++)  {
        if (data[pix] < datamin) {datamin = data[pix];}
        if (data[pix] > datamax) {datamax = data[pix];}
    }
    if (verbose) {
        std::cout << "Min and Max pixel values = " << datamin << "; " << datamax << std::endl;
    }
}


// Function: normalizeAndScaleData
void ImageData::normalizeAndScaleData(float*& dataVector)
{
    // ---- Find Max and Min Values:
    float datamin, datamax;

    this->getMaxMinValues(datamax, datamin, dataVector, nX*nY, false);

    // Get average and standard deviation:
    float avg = 0, sig = 0;
    for (int px = 0; px < nX*nY; px++) {
        avg += dataVector[px];
    }
    avg /= nX*nY;
    for (int px = 0; px<nX*nY; px++) {
        sig += powf(dataVector[px] - avg, 0.5);
        //sig += powf(dataVector[px] - avg, 2.0);
    }
    sig = pow(sig/(nX*nY), 0.5);
    int deviations = 5;
    if (datamin < avg-deviations*sig) {datamin = avg-deviations*sig;}
    if (datamax > avg+deviations*sig) {datamax = avg+deviations*sig;}

    // ---- ScaleImage Data Here (linear scaling):
    for (int px = 0; px<nX*nY; px++) {
        dataVector[px] = (dataVector[px]-datamin)/(datamax - datamin);
    }
}

unsigned char * ImageData::getByteArrayFromFloatArray(int bytesPerRow, float*& dataVector)
{
    unsigned char *pix = new unsigned char[nX*nY];
    for (int row = 0; row < nY; row++) {
        for (int col = 0; col < nX; col++)
            pix[row*bytesPerRow + col] = (unsigned char) 255*dataVector[row*nX + col];
    }
    return pix;
}

// Function: setThumbnail
void ImageData::setThumbnail(const int width, const int height) {
    QImage img;
    float *dataVector = this->getData();
    if (dataVector == NULL) {
        dataVector = new float[width*height];
    } else {
        normalizeAndScaleData(dataVector);
    }

    // Map to Byte Array (nX = cols of pixels in a row, nY = rows):
    int bytesPerPixel = 1; // = sizeof(unsigned char)
    int pixelsPerRow = nX;
    int bytesPerRow = bytesPerPixel*pixelsPerRow;
    unsigned char *pix = getByteArrayFromFloatArray(bytesPerRow, dataVector);

    // Calculate the Thumbnail Image:
    QImage *imgStart = new QImage(pix, nX, nY, bytesPerRow, QImage::Format_Indexed8);
    img = QPixmap::fromImage(*imgStart).scaled(width, height, Qt::KeepAspectRatio).toImage();

    _thumbnail = img;
}

EDIT: after userr1728854's comment below about range checking I edited the first part of the DataModelController::data(), to check if this was the issue.

My code now looks like (its easier to refer to below than modifying the original code, plus I didn't want to change the context of my question by changing what I posted):

QVariant DataModelController::data(const QModelIndex & index, int role) const
{
    cout << "model index = " << index.row() << endl; // add this to help troubleshoot
    if (!index.isValid() || index.row() > this->rowCount() || !this->modelContainsRow(index.row()) ) {
        return QVariant();
    }

So even if this is not the most robust way to add a range-check to the data() method, the line:

cout << "model index = " << index.row() << endl;

should at least print: "index.row()" when I resize the window, and it does not. So resizing the window does not appear to access the data() method and the program still crashes.


Solution

  • It happens that some Qt classes store indexes of already deleted items and can call methods like QAbstractItemModel::data passing those indexes as parameters. Your code lacks the range check on the row value of an index in that method, so you get "index out of range" error. It is also a good idea to put range checks in the rest of the code that deals with DataModelController::_modelData.