Reorderable QTreeWidget

By default, QTreeWidget reparents items whenever we drag them, i.e. the dragged item becomes a child of the item on which it was dropped. This article demonstrates how to change this behavior to sibling reordering, i.e. the dragged item will remain a sibling of the item on which it was dropped. To achieve this behavior we will need to subclass QTreeWidget.

Implementation details

Below is the header customdialog.h, which stores a pointer to an instance of QTreeWidget (details on how to subclass this will follow).

class CustomDialog : public QDialog
{
    Q_OBJECT
public:
    CustomDialog(QWidget *parent = Q_NULLPTR);
public slots:
    void save();
private:
    QTreeWidget* widget;
    QDialogButtonBox* buttonBox;
    QGroupBox* viewBox;
    QPushButton* saveButton;
    QPushButton* closeButton;

    void createTreeWidget();
    void createOtherWidgets();
    void createLayout();
    void createConnections();
};

In the constructor of CustomDialog we instantiate QTreeWidget, populate it with data, setup the layouts and connections.

CustomDialog::CustomDialog(QWidget *parent): QDialog(parent)
{
    setWindowTitle(tr("Reorderable QTreeWidget"));

    createTreeWidget();
    createOtherWidgets();
    createLayout();
    createConnections();

    resize(350,400);
}

The method createTreeWidget() can be seen below. After instantiating a CustomTreeWidget object we set the value of drag/drop mode to QAbstractItemView::InternalMove. The actual tree is populated with the help of QMap in which we store the key-value pair continent-airports. We then assign the data from the map to individual QTreeWidgetItems. Parent items represent the continents, while the child items represent the individual airports. The text representation of the items is set with the help of the method setText(). The first argument of the method is the column number (this is 0 as we are only using the 0-th column of the tree).

void CustomDialog::createTreeWidget(){
    widget = new CustomTreeWidget;
    widget->setDragDropMode(QAbstractItemView::InternalMove);
    widget->header()->close();

    QMap<QString, QStringList> treeMap;
    treeMap.insert("Asia", QStringList() << "Beijing Capital International Airport"
                   << "Dubai International Airport" << "Hong Kong International Airport"
                   << "Shanghai Pudong International Airport" << "Soekarno-Hatta International Airport"
                   << "Singapore Changi Airport");
    treeMap.insert("Europe", QStringList() << "Heathrow Airport" << "Charles de Gaulle Airport"
                   << "Amsterdam Airport Schiphol" << "Frankfurt Airport"
                   << "Istanbul Ataturk Airport" << "Adolfo Suarez Madrid–Barajas Airport");

    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);
        }
    }
}

Subclassing QTreeWidget

To make items reorderable in such a way that they remain siblings, we subclass the QTreeWidget and override the methods dragEnterEvent() and dropEvent(). We also store the pointer to the item that is being dragged.

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

In dragEnterEvent() we assign the pointer draggedItem to the currently dragged item. In dropEvent() we retrieve the item that lies on the position of the drop. If the parent of the dragged item and the item on the drop position match, we perform the drag and drop, i.e. we remove the dragged item from the original position and insert it at the new position (determined by QModelIndex::row()). If the parents do not match, (e.g. if we were trying to drop a European airport among Asian ones), the function won't do anything and will return.

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* dParent = draggedItem->parent();
        if(dParent){
            if(itemFromIndex(droppedIndex.parent()) != dParent)
                return;
            dParent->removeChild(draggedItem);
            dParent->insertChild(droppedIndex.row(), draggedItem);
        }
    }
}

That's it! Further details of the implementation can be found in the source files at the beginning of this article.


Created: 21.02.2017
© Walletfox.com, 2017