【问题标题】:Delete rows from QTreeView with custom model使用自定义模型从 QTreeView 中删除行
【发布时间】:2018-02-06 22:10:49
【问题描述】:

我是 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

ma​​in.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 进行彻底分析后,我得出结论,将索引 parentinternalPointer 存储在实际 Node 的索引中很重要,但在我的示例中,我使用 internalPointer 来存储 Node 本身。因此,假设 indernalPointer 中的信息与元素无关并且保持正确,Qt 内部实现在已删除元素的索引上调用 parent() 似乎是正确的。 (如果我错了,请纠正我。)

确认,在重写实现以在内部节点中存储指向父节点的指针后,此错误已消除。接受的答案中提供了对其他错误的更正。

【问题讨论】:

  • 根据调试器,错误是在getIndexInParent() 方法的++it; 中给出的,该方法由createIndex() 调用,您必须验证下一个元素是否存在。
  • @eyllanesc 我的调试器完全显示了这种行为,正如我在问题中指出的那样,从已删除的节点调用 getIndexInParent() 方法。 (从 TreeModel::parent() 作为 createIndex() 的第一个参数调用)。
  • 我已经和QAbstractItemModel 斗争了很长时间。由于我的数据模型和节点的延迟填充,Qt 示例没有太大帮助(因为我的数据模型可能有循环引用,必须仅根据用户请求解决)。经过大量调试(以及很多头痛),它现在稳定了(我希望)。最后一个重大改进是通过将qDebug()s 放入任何处理QModelIndex 的东西来实现的。我很震惊,例如parent() 甚至在既不是交互也不是修改的节点上被调用。

标签: c++ qt


【解决方案1】:

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

【讨论】:

  • 感谢您指出这个错误。这个问题确实存在于我的代码中。所以解决方案可能是将选定的索引复制到持久索引并迭代持久索引。然而,这只解决了部分问题:一次删除多个元素。实际上,所描述的错误仍然存​​在,如果您尝试一次仅删除单个元素(通过删除 impl->treeView->setSelectionMode(QAbstractItemView::ExtendedSelection); 从 widget.cpp).
  • 最好不要使用索引,而是使用一些元属性,例如“I'd”,它不可见,但唯一且永不改变。您获得选定的索引,然后解析 id 列表并使用您的 remove(list) 方法,删除您需要的所有内容。像这样。
  • 我编译了你的代码。你还有一些其他的问题。根节点必须具有无效索引(QModelIndex())。由于代码太多,我将其粘贴到pastebin:pastebin.com/UkL4hFXS
  • 你能说出具体在哪里看吗?现在我发现函数 parent() 中只有一个关键区别:if (!node->hasParent() || node->parent == &impl->root)。你是说有一个一般规则,每个无效索引和根索引都应该只由 QModelIndex() 指定吗?或者也许有一些例外?
  • Node* node = parent.isValid() ? static_cast&lt;Node*&gt;(parent.internalPointer()) : &amp;impl-&gt;root; 在 removeRows() 和 if (!node-&gt;hasParent() || node-&gt;parent == &amp;impl-&gt;root) { return QModelIndex(); } 在 parent()。因为Root必须是QModelInedx(),内部指针会返回0x0,所以需要在removeRows中指定指向它的指针。
【解决方案2】:

在 removeChilds() 中尝试将条件从 end

【讨论】:

  • 感谢您的回答,但 removeChilds 期望第二个参数是要删除的最后一个元素旁边的元素数。 (就像在 for (i = 0; i
猜你喜欢
  • 1970-01-01
  • 2016-04-27
  • 2020-01-30
  • 1970-01-01
  • 2021-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-31
相关资源
最近更新 更多