拖动重排QTreeWidgetItem后,内嵌QWidget消失如何解决?
哈哈,这个坑我之前踩过!QTreeWidget的setItemWidget看起来好用,但内置的拖动重排逻辑根本不会管你挂在项上的自定义控件——因为这些控件是直接绑定在视图层面的,拖动时模型只处理数据,视图刷新就把控件的关联丢了,自然就变成空行啦。给你两个解决方案,按需选:
方案一:手动保存控件关联(快速修改现有代码)
这个方案不用大改你现有的逻辑,只要加个映射表保存控件和项的关联,拖动完成后重新绑定就行:
- 先在你的
ToolsSelectorWidget类里加个成员变量,用来存每个项对应的所有列控件:
private: QMap<QTreeWidgetItem*, QList<QWidget*>> m_itemWidgetMap;
- 修改构造函数里创建项的代码,把控件存到映射表后再绑定:
ToolsSelectorWidget::ToolsSelectorWidget(QWidget *parent) : QTreeWidget(parent) { header()->hide(); setSelectionMode(QAbstractItemView::SingleSelection); setDragEnabled(true); viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setDragDropMode(QAbstractItemView::InternalMove); for(int i=0; i<4; ++i) { QTreeWidgetItem *part = new QTreeWidgetItem(this); // 简化flags的写法,效果和你原来的一样 part->setFlags(part->flags() & ~Qt::ItemIsDropEnabled); // 先创建所有控件并存到列表 QList<QWidget*> widgets; widgets << new QLabel("Part" + QString::number(i) + " Preview", this); widgets << new QLineEdit("Part" + QString::number(i) + " Name", this); widgets << new QCheckBox("Part" + QString::number(i) + " Visible", this); widgets << new QCheckBox("Part" + QString::number(i) + " Locked", this); // 绑定控件到项,同时存入映射表 for(int col = 0; col < widgets.size(); ++col) { setItemWidget(part, col, widgets[col]); } m_itemWidgetMap[part] = widgets; } }
- 修改
dropEvent,在默认拖动完成后重新绑定控件:
void ToolsSelectorWidget::dropEvent(QDropEvent *event) { QModelIndex droppedIndex = indexAt(event->pos()); if( !droppedIndex.isValid() || droppedIndex.parent().isValid()) { return; } // 先执行默认的拖动重排逻辑 QTreeWidget::dropEvent(event); // 遍历所有顶层项,重新把控件绑定到对应的项上 for(int i = 0; i < topLevelItemCount(); ++i) { QTreeWidgetItem* item = topLevelItem(i); if(m_itemWidgetMap.contains(item)) { QList<QWidget*> widgets = m_itemWidgetMap[item]; for(int col = 0; col < widgets.size(); ++col) { if(col < columnCount()) { setItemWidget(item, col, widgets[col]); } } } } // 强制刷新视图,确保控件正常显示 viewport()->update(); }
这样拖动完成后,控件就会重新绑定到移动后的项上,不会消失了。
方案二:使用自定义ItemDelegate(符合Qt MVC规范,长期维护更稳定)
setItemWidget本质是绕开了Qt的MVC模式,直接在视图上挂控件,时间长了容易出现数据和视图不一致的问题。用自定义Delegate可以让控件和项的数据绑定,拖动时数据跟着项走,控件自然也会跟着渲染:
- 先创建一个自定义Delegate类:
#include <QStyledItemDelegate> #include <QLabel> #include <QLineEdit> #include <QCheckBox> class ToolsItemDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit ToolsItemDelegate(QWidget *parent = nullptr) : QStyledItemDelegate(parent) {} // 创建对应列的编辑器控件 QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override { switch(index.column()) { case 0: { // Preview标签(只读) QLabel* label = new QLabel(parent); label->setFrameStyle(QFrame::Box); return label; } case 1: { // 名称输入框 QLineEdit* edit = new QLineEdit(parent); return edit; } case 2: case 3: { // 复选框 QCheckBox* check = new QCheckBox(parent); return check; } default: return QStyledItemDelegate::createEditor(parent, option, index); } } // 把项的数据同步到控件上 void setEditorData(QWidget *editor, const QModelIndex &index) const override { QVariant data = index.data(Qt::EditRole); switch(index.column()) { case 0: { if(auto label = qobject_cast<QLabel*>(editor)) { label->setText(data.toString()); } break; } case 1: { if(auto edit = qobject_cast<QLineEdit*>(editor)) { edit->setText(data.toString()); } break; } case 2: case 3: { if(auto check = qobject_cast<QCheckBox*>(editor)) { check->setChecked(data.toBool()); check->setText(index.data(Qt::DisplayRole).toString()); } break; } default: QStyledItemDelegate::setEditorData(editor, index); } } // 把控件的修改同步回项的数据 void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { switch(index.column()) { case 0: // 只读标签,不用保存 break; case 1: { if(auto edit = qobject_cast<QLineEdit*>(editor)) { model->setData(index, edit->text(), Qt::EditRole); } break; } case 2: case 3: { if(auto check = qobject_cast<QCheckBox*>(editor)) { model->setData(index, check->isChecked(), Qt::EditRole); } break; } default: QStyledItemDelegate::setModelData(editor, model, index); } } // 调整控件的位置和大小 void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override { editor->setGeometry(option.rect); } // 非编辑状态下也绘制控件(默认Delegate只在编辑时显示) void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { switch(index.column()) { case 0: { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); QLabel label; label.setText(index.data(Qt::EditRole).toString()); label.setFrameStyle(QFrame::Box); label.resize(opt.rect.size()); painter->save(); painter->translate(opt.rect.topLeft()); label.render(painter); painter->restore(); return; } case 2: case 3: { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); QCheckBox check; check.setChecked(index.data(Qt::EditRole).toBool()); check.setText(index.data(Qt::DisplayRole).toString()); check.resize(opt.rect.size()); painter->save(); painter->translate(opt.rect.topLeft()); check.render(painter); painter->restore(); return; } default: QStyledItemDelegate::paint(painter, option, index); } } };
- 修改
ToolsSelectorWidget的构造函数,不用setItemWidget,改用Delegate并设置项的初始数据:
ToolsSelectorWidget::ToolsSelectorWidget(QWidget *parent) : QTreeWidget(parent) { header()->hide(); setSelectionMode(QAbstractItemView::SingleSelection); setDragEnabled(true); viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setDragDropMode(QAbstractItemView::InternalMove); // 设置自定义Delegate setItemDelegate(new ToolsItemDelegate(this)); // 设置列数 setColumnCount(4); for(int i=0; i<4; ++i) { QTreeWidgetItem *part = new QTreeWidgetItem(this); part->setFlags(part->flags() & ~Qt::ItemIsDropEnabled); // 设置每列的数据,Delegate会自动根据数据渲染控件 part->setData(0, Qt::EditRole, "Part" + QString::number(i) + " Preview"); part->setData(1, Qt::EditRole, "Part" + QString::number(i) + " Name"); // 复选框需要同时设置显示文本和选中状态 part->setData(2, Qt::EditRole, true); part->setData(2, Qt::DisplayRole, "Part" + QString::number(i) + " Visible"); part->setData(3, Qt::EditRole, false); part->setData(3, Qt::DisplayRole, "Part" + QString::number(i) + " Locked"); } }
- 这时候你的
dropEvent可以完全不用修改!因为Delegate是和模型数据绑定的,拖动项的时候数据跟着移动,Delegate会自动在新位置渲染对应的控件,完全不会出现控件消失的问题,而且后续要修改控件状态、保存配置也更方便。
内容的提问来源于stack exchange,提问作者tsragravorogh
相关产品推荐
相关产品推荐

