Search code examples
pythonpyqtdrag-and-droppyqt5qtreewidget

Drag items between QTreeWidget and QListWidget in PyQt5?


I have a QListWidget and a QTreeWidget and I want to be able to drag one or multiple list items around within each one as well as between them. I have the internal drag and drop working, but I'm not sure how to drag between them.

enter image description here

When I print event.mimeData().formats() in my code below it says ['application/x-qabstractitemmodeldatalist']. I am stuck on how to extract the text and index of that item (it should be a QListWidgetItem or QTreeWidgetItem, right?) so I can delete the original from the source widget and add a new item to the destination widget, how do I do that?

I tried using code from here:
https://wiki.python.org/moin/PyQt/Handling%20Qt%27s%20internal%20item%20MIME%20type

So currently my code gets a QVariant items when I drag items between the widgets, but I still don't know what to do with those to get the item text and index.

from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTreeWidgetItem
import sys

class Tree(QtWidgets.QTreeWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setDragDropMode(self.DragDrop)
        self.setSelectionMode(self.ExtendedSelection)
        self.setAcceptDrops(True)

        for text in ['tree1','tree2','tree3']:
            treeItem = QtWidgets.QTreeWidgetItem(self, [text])
            treeItem.setFlags(treeItem.flags() & ~QtCore.Qt.ItemIsDropEnabled)
            self.addTopLevelItem(treeItem)

    def dropEvent(self, event):
        if event.source() == self:
            event.setDropAction(QtCore.Qt.MoveAction)
            super().dropEvent(event)
        elif isinstance(event.source(), QtWidgets.QListWidget):
            item = self.itemAt(event.pos())
            ix = self.indexAt(event.pos())
            col = 0 if item is None else ix.column()
            item = self.invisibleRootItem() if item is None else item
            ba = event.mimeData().data('application/x-qabstractitemmodeldatalist')
            data_items = decode_data(ba)
            for data_item in data_items:
                it = QtWidgets.QTreeWidgetItem()
                item.addChild(it)
                for data in data_items:
                    for r, v in data.items():
                        it.setData(col, r, v)


class List(QtWidgets.QListWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setDragDropMode(self.DragDrop)
        self.setSelectionMode(self.ExtendedSelection)
        self.setAcceptDrops(True)

        for text in ['list1','list2','list3']:
            self.addItem(text)

    def dropEvent(self, event):
        if event.source() == self:
            event.setDropAction(QtCore.Qt.MoveAction)
            QtWidgets.QListWidget.dropEvent(self, event)
        elif isinstance(event.source(), QtWidgets.QTreeWidget):
            item = self.itemAt(event.pos())
            row = self.row(item) if item else self.count()
            ba = event.mimeData().data('application/x-qabstractitemmodeldatalist')
            data_items = decode_data(ba)
            for i, data_item in enumerate(data_items):
                it = QtWidgets.QListWidgetItem()
                self.insertItem(row+i, it)
                for r, v in data_item.items():
                    it.setData(r,v)


def decode_data(bytearray):

    data = []
    item = {}

    ds = QtCore.QDataStream(bytearray)
    while not ds.atEnd():

        row = ds.readInt32()
        column = ds.readInt32()

        map_items = ds.readInt32()
        for i in range(map_items):
            key = ds.readInt32()

            value = QtCore.QVariant()
            ds >> value
            item[Qt.ItemDataRole(key)] = value

        data.append(item)
    return data

if __name__=='__main__':

    app = QtWidgets.QApplication(sys.argv)

    layout = QtWidgets.QHBoxLayout()
    layout.addWidget(Tree())
    layout.addWidget(List())

    container = QtWidgets.QWidget()
    container.setLayout(layout)
    container.show()

    app.exec_()

Solution

  • You have to create the items using the information returned by decode_data which are the roles and values of each item dragged:

    class Tree(QtWidgets.QTreeWidget):
        # ...
    
        def dropEvent(self, event):
            if event.source() == self:
                event.setDropAction(QtCore.Qt.MoveAction)
                super().dropEvent(event)
            elif isinstance(event.source(), QtWidgets.QListWidget):
                item = self.itemAt(event.pos())
                ix = self.indexAt(event.pos())
                col = 0 if item is None else ix.column()
                item = self.invisibleRootItem() if item is None else item            
                ba = event.mimeData().data('application/x-qabstractitemmodeldatalist')
                data_items = decode_data(ba)
                for data_item in data_items:
                    it = QtWidgets.QTreeWidgetItem()
                    item.addChild(it)
                    for data in data_items:
                        for r, v in data.items():
                            it.setData(col, r, v)
    
    class List(QtWidgets.QListWidget):
        # ... 
    
        def dropEvent(self, event):
            if event.source() == self:
                event.setDropAction(QtCore.Qt.MoveAction)
                QtWidgets.QListWidget.dropEvent(self, event)
            elif isinstance(event.source(), QtWidgets.QTreeWidget):
                item = self.itemAt(event.pos())
                row = self.row(item) if item else self.count()
                ba = event.mimeData().data('application/x-qabstractitemmodeldatalist')
                data_items = decode_data(ba)
                for i, data_item in enumerate(data_items):
                    it = QtWidgets.QListWidgetItem()
                    self.insertItem(row+i, it)
                    for r, v in data_item.items():
                        it.setData(r,v)