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!
...
// 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();
}
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::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();
}
}
}
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);
};
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
#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.
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
):
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With