6
votes

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.

2
I can't see ImageData.cpp in you text. Can u put whole example somewhere, for example, on github?Kakadu
I just posted imagedata.cpp above but I doubt if there is anything there that is contributing to the problem I am seeing. Nevertheless, there might be some code there that others might like to look at. Thanks.Bruce Dean
Run it with debugger when assertion in QList fails check the content of call stack. Maybe problem is not in the model list. Add this call stack here. Offtopic: range checking should have grater or equal: index.row() >= this->rowCount()Marek R
@MarekR, Thanks! I can get Q_ASSERT_X to print a message at the error condition = (i >= 0 && i < p.size()) but then the program aborts without going into the debugger (so I can't see the stack). I then used qInstallMessageHandler() in main but that only tells me that the error occurs at Q_ASSERT_X(). How can I stop the debugger before Q_ASSERT?Bruce Dean
@MarekR, ok making progress, I can see that it is the image provider class that needs a range check, I'll let you know if it works.Bruce Dean

2 Answers

7
votes

This is an explanation to document the actual "fix" that I implemented for my code. The discussion above doesn't really get into those details so I am writing that here in hopes that it may benefit others in the future.

First off, I rightfully awarded the bounty to userr1728854. Although the answer didn't give all the details on how to fix it, the answer was certainly correct in identifying the real reason my code was crashing, and for that I am grateful.

I also thank Marek R for his comment about setting the break-point at the assert_x function, that helped me track the crash to the image provider, whose code I intended to post above but must have forgot (didn't matter the image provider wasn't the source of the bug, just the symptom).

The origin of my problem was when I deleted objects (say rows 2,3) from my model. As shown in the example below, the model kept references to a set of index numbers that no longer exist (4, 5):

model indices after deleting rows

So next when I resized my QML window, the model provided index references that no longer exist to the image provider, giving the segfault. I.e., rather than pass the new relative indices 0, 1, 2, 3 (for the remaining items) my code was passing the old absolute indices: 0, 1, 4, 5.

That happens through the "thumbnail" rolename in the QML code for the image component:

Image {
    .
    .
    source: "image://provider/" + thumbnail
    .
    .
}

The fix was a simple 2-step process:

FIX (1): Replace the "thumbnail" rolename with the index variable of the QML model, whose values are automatically updated to match one-one to the model items when rows are removed or added. So finally I used this code:

Image {
    .
    .
    source: "image://provider/" + index
    .
    .
}

FIX (2): I needed to add a range check in the image provider to prevent the index values after deletion (initially a deleted index = -1) from getting passed to my model. The range check that I used is simply (there are probably better ones but this works ok for now):

bool ImageProvider::isValidIndex(DataModelController* model, int index) {
    int size = model->count();
    if (index >= size || index < 0)
        return false;
    return true;
}

This is a bit long-winded but I hope it helps somebody.

1
votes

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.