从 QAbstractListModel 中删除项目后 QML 崩溃

Posted

技术标签:

【中文标题】从 QAbstractListModel 中删除项目后 QML 崩溃【英文标题】:QML Crash after Deleting Items from QAbstractListModel 【发布时间】:2013-11-09 20:40:24 【问题描述】:

我在装有 OS-X 10.8.5 的 Mac 上运行 Qt 5.1.1 和 QtCreator 2.8.1。我有一个管理 ImageData 对象的 QAbstractListModel。在 main.cpp 中注册 ImageProvider 后,我可以使用 GridView 加载图像并在 QML 中很好地显示它们。

接下来我在视图中选择单个图像,例如,下面显示了几个带有橙色边框的选定图像:

然后是 C++ 模型函数:deleteSelected(),产生预期的结果:

但是,当我尝试调整窗口大小时,比如抓住其中一个角,我遇到了崩溃。堆栈跟踪显示:异常类型:EXC_CRASH (SIGABRT),我收到 Qt 错误:

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

所以也许我不正确地删除了模型项或未能通知模型更改,但我认为开始和结束 RemoveRows 发出了正确的信号来处理同步?毫无疑问,我在这方面遗漏了其他东西。

我还调用了 begin 和 end ResetModel,它可以防止应用程序在调整大小后崩溃,但在这种情况下,附加到模型的任何其他视图都会恢复为显示所有原始项目。

我已经为此寻找解决方案,尝试了很多代码实验,并研究了 here、here、here 以及其他几个地方发布的代码。

似乎无法使其正常工作,有什么建议吗?谢谢!

下面是一些相关代码:


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;


编辑:在 userr1728854 下面关于范围检查的评论之后,我编辑了 DataModelController::data() 的第一部分,以检查这是否是问题所在。

我的代码现在看起来像(它比修改原始代码更容易在下面引用,而且我不想通过更改我发布的内容来改变我的问题的上下文):

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();
    

因此,即使这不是向 data() 方法添加范围检查的最可靠方法,以下行:

cout

当我调整窗口大小时,至少应该打印:“index.row()”,但事实并非如此。所以调整窗口大小似乎并没有访问 data() 方法,程序仍然崩溃。

【问题讨论】:

我在你的文本中看不到 ImageData.cpp。你能把整个例子放在某个地方吗,例如,在 github 上? 我刚刚在上面发布了 imagedata.cpp,但我怀疑是否有任何问题导致我看到的问题。不过,那里可能有一些其他人可能想查看的代码。谢谢。 QList 中的断言失败时使用调试器运行它,检查调用堆栈的内容。也许问题不在模型列表中。在此处添加此调用堆栈。题外话:范围检查应该有更大的或等于index.row() &gt;= this-&gt;rowCount() @MarekR,谢谢!我可以让 Q_ASSERT_X 在错误条件 = (i >= 0 && i @MarekR,好的,取得进展,我可以看到需要范围检查的是图像提供程序类,如果它有效,我会告诉你。 【参考方案1】:

这是记录我为代码实现的实际“修复”的说明。上面的讨论并没有真正涉及到这些细节,所以我写在这里,希望它可以在未来对其他人有所帮助。

首先,我正确地将赏金授予userr1728854。尽管答案没有提供有关如何修复它的所有详细信息,但答案在确定我的代码崩溃的真正原因方面肯定是正确的,对此我很感激。

我还要感谢 Marek R 关于在 assert_x 函数设置断点的评论,这有助于我跟踪图像提供者的崩溃,我打算在上面发布它的代码但一定忘记了(没关系图片提供者不是错误的来源,只是症状)。

问题的根源是我从模型中删除对象(比如行 2,3)。如下例所示,模型保留了对一组不再存在的索引号的引用 (4, 5):

接下来,当我调整 QML 窗口的大小时,模型向图像提供者提供了不再存在的索引引用,从而产生了段错误。即,我的代码没有传递新的相对索引 0, 1, 2, 3(对于其余项目),而是传递旧的绝对索引:0, 1, 4, 5

这通过图像组件的 QML 代码中的“缩略图”角色名发生:

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

修复是一个简单的两步过程:

FIX (1):将“thumbnail”角色名替换为 QML 模型的 index 变量,当删除或添加行时,其值会自动更新以与模型项一一匹配。所以最后我使用了这段代码:

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

FIX (2):我需要在图像提供程序中添加范围检查,以防止删除后的索引值(最初删除的索引 = -1)传递给我的模型。我使用的范围检查很简单(可能有更好的,但目前还可以):

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

这有点啰嗦,但我希望它对某人有所帮助。

【讨论】:

哇,哇。我在这个问题上花了很长时间,当我添加新项目时我一直在崩溃,这只是在你第一次修复时使用图像提供程序而不匹配。事后看来,我应该使用最少的代码而不是复制工作视图,但在我的转发器中非常奇怪,它在 c++ 代码中崩溃并且 qml 调试器是静默的。无论哪种方式,答案都很好。 酷,很高兴它有帮助,这是一个简单但微妙的修复。我花了一段时间才弄清楚正确的事情。【参考方案2】:

有些 Qt 类会存储已删除项目的索引,并且可以调用像 QAbstractItemModel::data 这样的方法,将这些索引作为参数传递。您的代码在该方法中缺少对索引行值的范围检查,因此您会收到“索引超出范围”错误。 将范围检查放在处理DataModelController::_modelData 的其余代码中也是一个好主意。

【讨论】:

见我上面的编辑。我尝试了一个简单的范围检查,如上所述,调整窗口大小似乎不会触发对 data() 方法的调用,因此范围检查永远不会被调用。这有意义吗?除了 data() 之外,是否还有其他方法需要在窗口调整大小期间进行范围检查?感谢您的反馈,欢迎所有想法/讨论! 当时我的猜测是错误的。我有类似的情况,问题是视图试图访问已删除项目的索引。您还有 2 个可以按索引访问的地方:selectImage.onClicked 调用 toggleSelected 和在 selectImageBorder.Connections 中调用 isSelected。所以你可能也需要在那里检查范围。

以上是关于从 QAbstractListModel 中删除项目后 QML 崩溃的主要内容,如果未能解决你的问题,请参考以下文章

从 QAbstractListModel 中删除项目后 QML 崩溃

如何在 QAbstractListModel 中插入/删除/编辑行?

什么是最适合 QAbstractListModel 和 QListView 的 Qt 容器

使用 QAbstractListModel 从 python 访问 QML 中的列表元素

向 QAbstractListModel 子类添加新行时,QML 视图不会更新

为 QML ListView 实现 QAbstractListModel 子类?