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:
and then the C++ model function: deleteSelected(), produces the expected result:
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.
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