使用自定义模型

时间:2018-02-06 22:10:49

标签: c++ qt

我是Qt的新手。我正在尝试为树视图创建自定义模型,并支持删除行。我已根据示例http://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.htmlhttp://doc.qt.io/qt-5/qtwidgets-itemviews-editabletreemodel-example.html实施了该功能。此外,我已经创建了上下文菜单,其中包含在行上按下鼠标右键后删除行的选项。

现在,我几乎没有可重现的错误(没有确切的模式,但很容易获得)。当我开始随机删除模型中的行时,有时我的程序崩溃,有时我会收到以下消息输出:

QAbstractItemModel::endRemoveRows:  Invalid index ( 1 , 0 ) in model QAbstractItemModel(0x55555580db10)

当程序崩溃时,我几乎总是在功能

QModelIndex TreeModel::parent(const QModelIndex &child) const

继承自

QModelIndex QAbstractItemModel::parent(const QModelIndex &child) const

函数调用堆栈显示从

调用此函数
void QAbstractItemModel::beginRemoveRows(const QModelIndex &parent, int first, int last)

我称之为覆盖

bool TreeModel::removeRows(int row, int count, const QModelIndex &parent)

当我比较child.indernalPointer()(其中我存储指向内部树节点的指针,代表我的模型)的地址已经删除的节点时,很明显,由于某种原因,beginRemoveRows()使用已经无效的索引。

有一个非常类似错误的问题:QModelIndex becomes invalid when removing rows,但我无法理解我使用无效索引的原因和位置。

所以,我把最小的例子放在这个行为上(我已经花了很多精力将它最小化到这个大小并使代码清晰,对不起它很长)。

tree.pro

QT       += core gui widgets
TARGET = tree
TEMPLATE = app
SOURCES +=  main.cpp  widget.cpp  treemodel.cpp
HEADERS +=  widget.h  treemodel.h

treemodel.h

#ifndef TREEMODEL_H
#define TREEMODEL_H

#include <QAbstractItemModel>

class TreeModel : public QAbstractItemModel
{
public:
    TreeModel();
    ~TreeModel();

    QModelIndex index(int row, int column, const QModelIndex &parent) const override;
    QModelIndex parent(const QModelIndex &child) const override;
    int rowCount(const QModelIndex &parent) const override;
    int columnCount(const QModelIndex &parent) const override;
    QVariant data(const QModelIndex &index, int role) const override;

    bool setData(const QModelIndex &index, const QVariant &value, int role) override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;
    bool removeRows(int row, int count, const QModelIndex &parent) override;

private:
    class Impl;
    Impl* impl = nullptr;
};

#endif // TREEMODEL_H

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void projectTreeMenuRequested(const QPoint& point);
    void eraseItem();
private:
    class Impl;
    Impl* impl;
};

#endif // WIDGET_H

的main.cpp

#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

treemodel.cpp

#include "treemodel.h"

#include <cassert>
#include <string>
#include <list>
#include <memory>

namespace {

struct Node {
    Node(const std::string& name)
        : text(name)
    {
    }
    ~Node() {
    }

    Node& append(const std::string& name) {
        child.emplace_back(name);
        Node& n = child.back();
        n.parent = this;
        return n;
    }
    size_t getChildNum() const {
        return child.size();
    }
    bool hasParent() const {
        return parent != nullptr;
    }
    Node& getParent() {
        assert(hasParent());
        return *parent;
    }
    size_t getIndexInParent() const {
        if (parent) {
            size_t index = 0;
            Childs::iterator it = parent->child.begin();
            while (it != parent->child.end()) {
                if (&*it == this) {
                    return index;
                }
                ++it;
                ++index;
            }
        }
        return 0;
    }
    Node& getChild(size_t i) {
        assert(i < child.size());
        Childs::iterator it = child.begin();
        std::advance(it, i);
        return *it;
    }
    void setText(std::string name) {
        this->text = std::move(name);
    }
    std::string getText() const {
        return text;
    }
    void remove() {
        assert(hasParent());
        Node& p = getParent();
        for (Childs::iterator it = p.child.begin(); it != p.child.end(); ++it) {
            if (&*it == this) {
                p.child.erase(it);
                return;
            }
        }
        assert(0); // Child for remove not found
    }
    bool removeChilds(size_t start, size_t end) {
        if (start < end && end <= child.size()) {
            Childs::iterator it1 = child.begin();
            assert(it1 != child.end());
            std::advance(it1, start);
            assert(it1 != child.end());
            Childs::iterator it2 = it1;
            std::advance(it2, end - start);
            child.erase(it1, it2);
            return true;
        } else {
            return false;
        }
    }

    static const int Columns = 1;

private:
    using Childs = std::list<Node>;

    std::string text;
    Node* parent = nullptr;
    Childs child;
};

} // namespace

struct TreeModel::Impl {
    Impl()
        : root("Root")
    {
        fill(root);
    }

    void fill(Node& from, std::string str = "", int depth = 0) {
        if (depth == 10) return;
        for (int j = 0; j != 5; ++j) {
            std::string name = str + std::to_string(j);
            fill(from.append(name), name, depth+1);
        }
    }

    Node root;
};

TreeModel::TreeModel()
    : impl(new Impl)
{

}

TreeModel::~TreeModel()
{
    delete impl;
}

QModelIndex
TreeModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent)) {
        return QModelIndex();
    } else {
        Node* node = nullptr;
        if (!parent.isValid()) {
            node = &impl->root;
        } else {
            node = static_cast<Node*>(parent.internalPointer());
        }
        return createIndex(row, column, &node->getChild(row));
    }
}

QModelIndex TreeModel::parent(const QModelIndex &child) const
{
    if (!child.isValid()) {
        return QModelIndex();
    }
    Node* node = static_cast<Node*>(child.internalPointer());
    if (!node->hasParent()) {
        return QModelIndex();
    }
    return createIndex(node->getIndexInParent(),
                       child.column(),
                       &node->getParent());
}

int TreeModel::rowCount(const QModelIndex &parent) const
{
    Node* p = nullptr;
    if (parent.isValid()) {
        p = static_cast<Node*>(parent.internalPointer());
    } else {
        p = &impl->root;
    }
    return p->getChildNum();
}

int TreeModel::columnCount(const QModelIndex &) const
{
    return Node::Columns;
}

QVariant TreeModel::data(const QModelIndex &index, int role) const
{
    if (index.isValid()) {
        Node* node = static_cast<Node*>(index.internalPointer());
        switch (role) {
        case Qt::DisplayRole:
        case Qt::EditRole:
            return QString::fromUtf8(node->getText().data(),
                                     node->getText().size());
            break;
        }
    }
    return QVariant();
}

bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role != Qt::EditRole)
        return false;

    Node* node = nullptr;
    if (index.isValid()) {
        node = static_cast<Node*>(index.internalPointer());
    } else {
        node = &impl->root;
    }
    node->setText(value.toString().toStdString());

    emit dataChanged(index, index);

    return true;
}

Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return 0;

    return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
}

bool TreeModel::removeRows(int row, int count, const QModelIndex &parent)
{
    Node* node = nullptr;
    QModelIndex correctParent;
    if (parent.isValid()) {
        node = static_cast<Node*>(parent.internalPointer());
        correctParent = parent;
    } else {
        node = &impl->root;
        correctParent = QModelIndex();
    }

    beginRemoveRows(correctParent, row, row + count - 1); // [row, row + count - 1]
    bool success = node->removeChilds(row, row + count); // [row, row + count)
    endRemoveRows();

    return success;
}

widget.cpp

#include "widget.h"

#include <QVBoxLayout>
#include <QTreeView>
#include <QPoint>
#include <QMenu>

#include "treemodel.h"

struct Widget::Impl {
    QVBoxLayout* layout;
    QTreeView* treeView;
    TreeModel* treeModel;
};

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , impl(new Impl)
{
    impl->layout = new QVBoxLayout(this);
    impl->treeView = new QTreeView;
    impl->treeModel = new TreeModel;

    impl->layout->addWidget(impl->treeView);
    impl->treeView->setModel(impl->treeModel);
    impl->treeView->setSelectionMode(QAbstractItemView::ExtendedSelection);
    impl->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(impl->treeView, SIGNAL(customContextMenuRequested(const QPoint&)),
            this, SLOT(projectTreeMenuRequested(const QPoint&)));
}

Widget::~Widget()
{
    delete impl->treeModel;
    delete impl;
}

void Widget::projectTreeMenuRequested(const QPoint &point)
{
    QPoint globalPos = impl->treeView->mapToGlobal(point);

    QMenu myMenu;
    myMenu.addAction("Erase",  this, SLOT(eraseItem()));

    myMenu.exec(globalPos);
}

void Widget::eraseItem()
{
    for (QModelIndex index : impl->treeView->selectionModel()->selectedIndexes()) {
        impl->treeModel->removeRow(index.row(), index.parent());
    }
}

修改

我想到了两种解决问题的方法。第一种是直接方法,当有人指出我不正确使用Qt API时。第二种方法是,如果某人编写了这个功能的独立实现(具有无限嵌套和删除能力的树),我将尝试弄清楚与其他实现相比我做错了什么。

编辑2

在对QStandardItemModel进行彻底分析后,我得出结论,存储在实际Node的索引 parent internalPointer 中很重要,但在我的示例中我使用 internalPointer 来存储Node本身。因此,对于Qt内部实现来说,对已经删除的元素的索引调用parent()似乎是正确的,假设 indernalPointer 中的信息与元素无关并且保持正确。 (如果我错了,请纠正我。)

  

确认,在重写实现以将指针存储到内部节点中的父节点之后,这个bug被消除了。在接受的答案中提供了对其他错误的更正。

2 个答案:

答案 0 :(得分:1)

您在Widget :: eraseItem()中使用for(index:selectedIndexes()),但在删除某些内容后,索引会发生变化,因此您的索引变为无效。而且,在迭代它时更改容器是一种不好的做法。

答案 1 :(得分:0)

在removeChilds()中尝试将条件从end&lt; = child.size()更改为end&lt; child.size()