Search code examples
c++qtqt5qtreewidgetqtreewidgetitem

Drag and drop of QTreeWidgetItem does not work properly


I subclassed a QTreeWidget in order to reimplement the protected functionalities of drag and drop in order to drag drop parents and children of a QTreeWidget.

It almost works and the problem is that as soon as I try to drag and drop either children or parents, they are erased as soon as I drop them. Source code can be found here in case you need to see what is going on.

Also if I try to drag-drop the parent, it unfortunately does not move and nothing happens.

See below the strange effect for the children:

I drag the child "Original" to drop it right under "Sample" and this procedure seems to work correctly:

ddrop

After dropping "Original" as you can see the index seems to be there but it seems to be partially erased:

ddrop2

I am not sure exactly of what is going on and why the children seems to not completely be dropped correctly and why the parents are literally not moving.

Below the code of the minimal verifiable example:

mainwindow.h

#include <QMainWindow>

class QTreeWidget;
class QTreeWidgetItem;


QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
    QTreeWidgetItem *createItem(const QString &name, const QString &iconPath);
    
private:
    Ui::MainWindow *ui;
    QTreeWidget *widget;
    QString m_Name;
    QString m_Path;

    };
#endif // MAINWINDOW_H

mainwindow.cpp

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    ui->treeWidget->setSelectionMode(QAbstractItemView::SingleSelection);
    ui->treeWidget->setDragEnabled(true);
    ui->treeWidget->viewport()->setAcceptDrops(true);
    ui->treeWidget->setDropIndicatorShown(true);
    ui->treeWidget->setDragDropMode(QAbstractItemView::InternalMove);

    auto *top1 = createItem("Images", "/home/ui/qrc/laserscan.png");
    auto *top2 = createItem("Path", "/home/ui/qrc/laserscan.png");

    top1->addChild(createItem("Original", "/home/ui/qrc/laserscan.png"));
    top1->addChild(createItem("Sample", "/home/ui/qrc/laserscan.png"));

    top2->addChild(createItem("Left Side", "/home/ui/qrc/laserscan.png"));
    top2->addChild(createItem("Right Side", "/home/ui/qrc/laserscan.png"));

    ui->treeWidget->addTopLevelItems({ top1, top2 });

   // Below I am assigning to each parent/children a checkbox
    const int n_tops = ui->treeWidget->topLevelItemCount();
    for(int a = 0; a<n_tops; ++a) {
        const int n_children = ui->treeWidget->topLevelItem(a)->childCount();
        qtreewidgetitem_assign_qcheckbox(ui->treeWidget, ui->treeWidget->topLevelItem(a));
        for(int b = 0; b<n_children; ++b) {
            qtreewidgetitem_assign_qcheckbox(ui->treeWidget, ui->treeWidget->topLevelItem(a)->child(b));
        }
    }
}

MainWindow::~MainWindow()
{
    delete ui;
}

QTreeWidgetItem *MainWindow::createItem(const QString &name, const QString &iconPath)
{
    auto *item = new QTreeWidgetItem(QStringList{name});
    item->setIcon(0, QIcon(iconPath));
    return item;
}

maphelpers.h

#include <QTreeWidget>
#include <QTreeWidgetItem>

void qtreewidgetitem_assign_qcheckbox(QTreeWidget *tree_widget, QTreeWidgetItem *tree_item);

#endif // MAPHELPERS_H

maphelpers.cpp

#include <QTreeWidget>

void qtreewidgetitem_assign_qcheckbox(QTreeWidget *tree_widget, QTreeWidgetItem *tree_item)
{
    MyCheckBoxMap *myCheckBoxMap = new MyCheckBoxMap(tree_item);
    tree_widget->setItemWidget(tree_item, MAP_COLUMN, myCheckBoxMap);
}

Below how I subclassed QTreeWidget:

customtreewidget.h

#include <QTreeWidget>

class CustomTreeWidget : public QTreeWidget
{
public:
    CustomTreeWidget(QWidget *parent = Q_NULLPTR);
protected:
    void dragEnterEvent(QDragEnterEvent *event) override;
    void dropEvent(QDropEvent *event) override;
private:
    QTreeWidgetItem *draggedItem;
};

#endif // CUSTOMTREEWIDGET_H

customtreewidget.cpp

CustomTreeWidget::CustomTreeWidget(QWidget *parent)
{
    QTreeWidgetItem* parentItem = new QTreeWidgetItem();
    parentItem->setText(0, "Test");
    parentItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled);

    int num_rows = topLevelItemCount();

    for(int i = 0; i < num_rows; ++i)
    {
      int nChildren = topLevelItem(i)->childCount();
      QTreeWidgetItem* pItem = new QTreeWidgetItem(parentItem);
      for(int j = 0; j < nChildren; ++j) {
          pItem->setText(0, QString("Number %1").arg(i) );
          pItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
          pItem->addChild(pItem);
          topLevelItem(i)->child(j);
      }
    }
}

void CustomTreeWidget::dragEnterEvent(QDragEnterEvent *event)
{
    draggedItem = currentItem();
    QTreeWidget::dragEnterEvent(event);
}

void CustomTreeWidget::dropEvent(QDropEvent *event)
{
    QModelIndex droppedIndex = indexAt(event->pos());
    if(!droppedIndex.isValid()) {
        return;
    }

    if(draggedItem) {
        QTreeWidgetItem *mParent = draggedItem->parent();
        if(mParent) {
            if(itemFromIndex(droppedIndex.parent()) != mParent)
                return;
                mParent->removeChild(draggedItem);
                mParent->insertChild(droppedIndex.row(), draggedItem);
        }
    }
}

What I have done so far and what I have tried:

1) According to official documentation it is possible to use QTreeWidgetItemIterator Class and i tried to go that way and in fact you can see below my initial implementation idea, but this didn't really bring me anywhere:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    auto *top1 = createItem("Images", "/home/ui/qrc/laserscan.png");
    auto *top2 = createItem("Path", "/home/ui/qrc/laserscan.png");
    
    top1->addChild(createItem("Original", "/home/ui/qrc/laserscan.png"));
    top1->addChild(createItem("Sample", "/home/ui/qrc/laserscan.png"));
    
    top2->addChild(createItem("Left Side", "/home/ui/qrc/laserscan.png"));
    top2->addChild(createItem("Right Side", "/home/ui/qrc/laserscan.png"));
    
    ui->treeWidget->addTopLevelItems({ top1, top2 });

    const int n_tops = ui->treeWidget->topLevelItemCount();

    // Below I am assigning to each parent/children a checkbox
    for(int a = 0; a<n_tops; ++a) {
        const int n_children = ui->treeWidget->topLevelItem(a)->childCount();
        qtreewidgetitem_assign_qcheckbox(ui->treeWidget, ui->treeWidget->topLevelItem(a));
        for(int b = 0; b<n_children; ++b) {
            qtreewidgetitem_assign_qcheckbox(ui->treeWidget, ui->treeWidget->topLevelItem(a)->child(b));
        }
    }

    QTreeWidgetItem *parentItem = Q_NULLPTR;
    QTreeWidgetItem *childItem = Q_NULLPTR;

    QTreeWidgetItemIterator it(ui->treeWidget);
       while (*it) {
           parentItem = new QTreeWidgetItem(ui->treeWidget);
           //parentItem->setText(0, it.key());
           foreach (const auto &str, it) {
               childItem = new QTreeWidgetItem;
               childItem->setText(0, str);
               parentItem->addChild(childItem);
           }
       }
}

2) After further research in the official documentation I decided to approach the problem applying the QMapIterator Class I wanted to keep the constructor as I structured initially and figured that passing through a QMap meant to rewrite the constructor in a different way, basically the implementation idea below, but didn't want to pass that way:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    auto *top1 = createItem("Images", "/home/ui/qrc/laserscan.png");
    auto *top2 = createItem("Path", "/home/ui/qrc/laserscan.png");
    
    top1->addChild(createItem("Original", "/home/ui/qrc/laserscan.png"));
    top1->addChild(createItem("Sample", "/home/ui/qrc/laserscan.png"));
    
    top2->addChild(createItem("Left Side", "/home/ui/qrc/laserscan.png"));
    top2->addChild(createItem("Right Side", "/home/ui/qrc/laserscan.png"));
    
    ui->treeWidget->addTopLevelItems({ top1, top2 });
    ui->treeWidget->addTopLevelItems({ top1, top2 });

    const int n_tops = ui->treeWidget->topLevelItemCount();

    // Below I am assigning to each parent/children a checkbox
    for(int a = 0; a<n_tops; ++a) {
        const int n_children = ui->treeWidget->topLevelItem(a)->childCount();
        qtreewidgetitem_assign_qcheckbox(ui->treeWidget, ui->treeWidget->topLevelItem(a));
        for(int b = 0; b<n_children; ++b) {
            qtreewidgetitem_assign_qcheckbox(ui->treeWidget, ui->treeWidget->topLevelItem(a)->child(b));
        }
    }

    QTreeWidgetItem *parentItem = Q_NULLPTR;
    QTreeWidgetItem *childItem = Q_NULLPTR;

    QMapIterator<QString, QStringList> i(treeMap);
    while (i.hasNext()) {
        i.next();
        parentItem = new QTreeWidgetItem(widget);
        parentItem->setText(0, i.key());
        foreach (const auto &str, i.value()) {
            childItem = new QTreeWidgetItem;
            childItem->setText(0, str);
            parentItem->addChild(childItem);
        }
    }
}

3) After more research I cam across this source, and this other source which was useful (in particular the last post) as guidance to the implementation of the subclassing of the QTreeWidget.

Now I am stuck as I am trying to figure out why, despite all the things I tried, I achieved a 90% working application and, therefore, what is missing about it?

Thanks for providing guidance on how to solve this problem.


Solution

  • I think the checkboxes are handling the mouse events and don't let the events go through to the item. To have the desired behavior, you should remove this part:

    const int n_tops = ui->treeWidget->topLevelItemCount();
    
    for(int a = 0; a<n_tops; ++a) {
        const int n_children = ui->treeWidget->topLevelItem(a)->childCount();
        qtreewidgetitem_assign_qcheckbox(ui->treeWidget, ui->treeWidget->topLevelItem(a));
        for(int b = 0; b<n_children; ++b) {
            qtreewidgetitem_assign_qcheckbox(ui->treeWidget, ui->treeWidget->topLevelItem(a)->child(b));
        }
    }
    

    And in the createItem() function:

    QTreeWidgetItem *MainWindow::createItem(const QString &name, const QString &iconPath)
    {
        auto *item = new QTreeWidgetItem(QStringList{name});
        item->setCheckState(0, Qt::Unchecked);
        item->setIcon(0, QIcon(iconPath));
        item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
        return item;
    }