【发布时间】:2018-02-06 22:10:49
【问题描述】:
我是 Qt 的新手。我正在尝试为支持行删除的树视图创建自定义模型。我已经根据示例http://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.html 和http://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 进行彻底分析后,我得出结论,将索引 parent 的 internalPointer 存储在实际 Node 的索引中很重要,但在我的示例中,我使用 internalPointer 来存储 Node 本身。因此,假设 indernalPointer 中的信息与元素无关并且保持正确,Qt 内部实现在已删除元素的索引上调用 parent() 似乎是正确的。 (如果我错了,请纠正我。)
确认,在重写实现以在内部节点中存储指向父节点的指针后,此错误已消除。接受的答案中提供了对其他错误的更正。
【问题讨论】:
-
根据调试器,错误是在
getIndexInParent()方法的++it;中给出的,该方法由createIndex()调用,您必须验证下一个元素是否存在。 -
@eyllanesc 我的调试器完全显示了这种行为,正如我在问题中指出的那样,从已删除的节点调用 getIndexInParent() 方法。 (从 TreeModel::parent() 作为 createIndex() 的第一个参数调用)。
-
我已经和
QAbstractItemModel斗争了很长时间。由于我的数据模型和节点的延迟填充,Qt 示例没有太大帮助(因为我的数据模型可能有循环引用,必须仅根据用户请求解决)。经过大量调试(以及很多头痛),它现在稳定了(我希望)。最后一个重大改进是通过将qDebug()s 放入任何处理QModelIndex的东西来实现的。我很震惊,例如parent()甚至在既不是交互也不是修改的节点上被调用。