Search code examples
pythonpyqt5drag-and-dropqtreewidget

Drag and Drop between QTreeWidgets


Is it possible two drag and drop items between two QTreeWidgets? I have one tree-widget Tree1 and a second one Tree2. Tree1 is somehow the master which contains all elements (only top-level elements, child elements are disabled). Only sorting should be possible (no edit, move, copy etc.). Tree2 should contain items which are added by drag and drop from Tree1. The idea is that if one item gets moved from Tree1 to Tree2, it gets removed from Tree1. In Tree2, items can be moved (nothing more).

I have already checked the internet but could not really find a solution. I tried to override the drag and drop methods of the QTreeWidget class, but I do not understand the drag-drop mechanism at the moment. I do not see any event if I drag an item form Tree1 to Tree2.

from PyQt5 import QtCore, QtGui, QtWidgets

class TreeWidget(QtWidgets.QTreeWidget):
    def dropEvent(self, event):
        if event.source()==self:
            super().dropEvent(event)
        elif isinstance(event.source(), TreeWidget):
            print(True)

def create_treeWidget_1():
    treeWidget=TreeWidget()
    treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
    treeWidget.setAlternatingRowColors(True)
    treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    treeWidget.setHeaderHidden(False)
    treeWidget.setDragEnabled(True)

    treeWidget.headerItem().setText(0, "Header 0")
    treeWidget.headerItem().setText(1, "Header 1")
    treeWidget.headerItem().setText(2, "Header 2")

    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    treeWidget.topLevelItem(0).setText(0, "y1")
    treeWidget.topLevelItem(0).setText(1, "y2")
    treeWidget.topLevelItem(0).setText(2, "y3")
    treeWidget.topLevelItem(1).setText(0, "z1")
    treeWidget.topLevelItem(1).setText(1, "z2")
    treeWidget.topLevelItem(1).setText(2, "z3")
    
    return treeWidget

def create_treeWidget_2():
    treeWidget=TreeWidget()
    treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
    treeWidget.setAlternatingRowColors(True)
    treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    treeWidget.setHeaderHidden(False)
    treeWidget.setDragEnabled(True)
    treeWidget.viewport().setAcceptDrops(True)
    treeWidget.setDropIndicatorShown(True)
    treeWidget.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)

    treeWidget.headerItem().setText(0, "Header 0")
    treeWidget.headerItem().setText(1, "Header 1")
    treeWidget.headerItem().setText(2, "Header 2")

    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    treeWidget.topLevelItem(0).setText(0, "a")
    treeWidget.topLevelItem(0).setText(1, "b")
    treeWidget.topLevelItem(0).setText(2, "c")
    treeWidget.topLevelItem(1).setText(0, "1")
    treeWidget.topLevelItem(1).setText(1, "2")
    treeWidget.topLevelItem(1).setText(2, "3")
    
    return treeWidget

if __name__ == "__main__":

    import sys

    app=QtWidgets.QApplication(sys.argv)
    app.setStyle("fusion")
    tree1=create_treeWidget_1()
    tree2=create_treeWidget_2()
    tree1.show()
    tree2.show()
    sys.exit(app.exec_())

09172023 Update: I continued with my project. Now I am faced with a new problem which is related to the initial question. So I think it fits perfectly to this thread. See below the updated code which solves the initial problem and includes the new one.

from PyQt5 import QtCore, QtGui, QtWidgets
    

class TreeWidget(QtWidgets.QTreeWidget):

    itemDropped=QtCore.pyqtSignal()

    def __init__(self):
        QtWidgets.QTreeWidget.__init__(self)
        
    def dropEvent(self, event):
        super().dropEvent(event)

        if event.source() is not self:
            item=self.itemAt(event.pos())
            
            if self.dropIndicatorPosition()==self.BelowItem:
                item=self.itemBelow(item)
                
            elif self.dropIndicatorPosition()==self.OnViewport:
                item=self.topLevelItem(self.topLevelItemCount()-1)

            item.setFlags(item.flags()&~QtCore.Qt.ItemIsDropEnabled)

        self.itemDropped.emit()


def create_treeWidget_1():
    treeWidget=QtWidgets.QTreeWidget()
    treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
    treeWidget.setAlternatingRowColors(True)
    treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    treeWidget.setHeaderHidden(False)
    
    treeWidget.setDragEnabled(True)
    treeWidget.setDefaultDropAction(QtCore.Qt.MoveAction)
    treeWidget.setAcceptDrops(False)

    treeWidget.headerItem().setText(0, "Header 0")
    treeWidget.headerItem().setText(1, "Header 1")
    treeWidget.headerItem().setText(2, "Header 2")

    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    treeWidget.topLevelItem(0).setText(0, "y1")
    treeWidget.topLevelItem(0).setText(1, "y2")
    treeWidget.topLevelItem(0).setText(2, "y3")
    treeWidget.topLevelItem(1).setText(0, "z1")
    treeWidget.topLevelItem(1).setText(1, "z2")
    treeWidget.topLevelItem(1).setText(2, "z3")
    
    return treeWidget


def create_treeWidget_2():
    treeWidget=TreeWidget()
    treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
    treeWidget.setAlternatingRowColors(True)
    treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    treeWidget.setHeaderHidden(False)

    treeWidget.setDragEnabled(True)
    treeWidget.viewport().setAcceptDrops(True)
    treeWidget.setDropIndicatorShown(True)
    treeWidget.setDefaultDropAction(QtCore.Qt.MoveAction)
    treeWidget.setAcceptDrops(True)

    treeWidget.itemDropped.connect(updateView)

    treeWidget.headerItem().setText(0, "Header 0")
    treeWidget.headerItem().setText(1, "Header 1")
    treeWidget.headerItem().setText(2, "Header 2")

    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    treeWidget.topLevelItem(0).setText(0, "a")
    treeWidget.topLevelItem(0).setText(1, "b")
    treeWidget.topLevelItem(0).setText(2, "c")
    treeWidget.topLevelItem(1).setText(0, "1")
    treeWidget.topLevelItem(1).setText(1, "2")
    treeWidget.topLevelItem(1).setText(2, "3")
    
    return treeWidget


def create_treeWidget_3():
    treeWidget=TreeWidget()
    treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
    treeWidget.setAlternatingRowColors(True)
    treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    treeWidget.setHeaderHidden(False)

    treeWidget.setDragEnabled(True)
    treeWidget.viewport().setAcceptDrops(True)
    treeWidget.setDropIndicatorShown(True)
    treeWidget.setDefaultDropAction(QtCore.Qt.MoveAction)
    treeWidget.setAcceptDrops(True)

    treeWidget.itemDropped.connect(updateView)

    treeWidget.headerItem().setText(0, "Header 0")
    treeWidget.headerItem().setText(1, "Header 1")
    treeWidget.headerItem().setText(2, "Header 2")
    
    return treeWidget


def updateView():
    try:
        print('update')
        print('    update left:')
            
        iterator=QtWidgets.QTreeWidgetItemIterator(tree2)

        while iterator.value():
            item=iterator.value()
            for column in range(item.columnCount()): print('        '+item.text(column))
            print()
            iterator+=1
            
        print('\n    update right:')
        iterator=QtWidgets.QTreeWidgetItemIterator(tree3)

        while iterator.value():
            item=iterator.value()
            for column in range(item.columnCount()): print('        '+item.text(column))
            print()
            iterator+=1

        print()

        print('-----')
    except BaseException as e: print(e)


if __name__ == "__main__":
    import sys

    app=QtWidgets.QApplication(sys.argv)
    app.setStyle("fusion")
    tree1=create_treeWidget_1()
    tree2=create_treeWidget_2()
    tree3=create_treeWidget_3()
    tree1.show()
    tree2.show()
    tree3.show()
    sys.exit(app.exec_())

I added a third tree widget called Tree3. Everytime an item is moved from Tree2 to Tree3 or vice versa a signal gets emited. The idea is that every time items get moved the new distribution of the items between Tree2 and Tree3 is collected. But for some rease if an item gets moved the tree which owned the item does not gets updated.

Example: Move item from Tree2 to Tree3. The screen output shows that the item appears in Tree2 and in Tree3. If the signal gets emitted again by moving an item internally (inside Tree2 or Tree3) the item is removed in Tree2. I think the content of Tree2 is not really updated when the drop event emits the signal. Is there an additional event which appears when Tree2 is updated that can be used to emit the signal?

09252023 Solution:

from PyQt5 import QtCore, QtGui, QtWidgets

counter=0

class TreeWidget(QtWidgets.QTreeWidget):
    
    itemDropped=QtCore.pyqtSignal()

    def __init__(self):
        QtWidgets.QTreeWidget.__init__(self)       
        
    def dropEvent(self, event):
        super().dropEvent(event)

        if event.source() is not self:
            item=self.itemAt(event.pos())
            
            if self.dropIndicatorPosition()==self.BelowItem:
                item=self.itemBelow(item)
                
            elif self.dropIndicatorPosition()==self.OnViewport:
                item=self.topLevelItem(self.topLevelItemCount()-1)

            item.setFlags(item.flags()&~QtCore.Qt.ItemIsDropEnabled)

        self.itemDropped.emit()

def create_treeWidget_1():
    treeWidget=TreeWidget()
    treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
    treeWidget.setAlternatingRowColors(True)
    treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    treeWidget.setHeaderHidden(False)
    
    treeWidget.setDragEnabled(True)
    treeWidget.setDefaultDropAction(QtCore.Qt.MoveAction)
    treeWidget.setAcceptDrops(False)

    treeWidget.headerItem().setText(0, "Header 0")
    treeWidget.headerItem().setText(1, "Header 1")
    treeWidget.headerItem().setText(2, "Header 2")

    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    treeWidget.topLevelItem(0).setText(0, "y1")
    treeWidget.topLevelItem(0).setText(1, "y2")
    treeWidget.topLevelItem(0).setText(2, "y3")
    treeWidget.topLevelItem(1).setText(0, "z1")
    treeWidget.topLevelItem(1).setText(1, "z2")
    treeWidget.topLevelItem(1).setText(2, "z3")

    treeWidget.model().rowsRemoved.connect(buffer)
    
    return treeWidget

def create_treeWidget_2():
    treeWidget=TreeWidget()
    treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
    treeWidget.setAlternatingRowColors(True)
    treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    treeWidget.setHeaderHidden(False)

    treeWidget.setDragEnabled(True)
    treeWidget.viewport().setAcceptDrops(True)
    treeWidget.setDropIndicatorShown(True)
    treeWidget.setDefaultDropAction(QtCore.Qt.MoveAction)
    treeWidget.setAcceptDrops(True)

    treeWidget.headerItem().setText(0, "Header 0")
    treeWidget.headerItem().setText(1, "Header 1")
    treeWidget.headerItem().setText(2, "Header 2")

    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    item_0=QtWidgets.QTreeWidgetItem(treeWidget)
    item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsEnabled)
    treeWidget.topLevelItem(0).setText(0, "a")
    treeWidget.topLevelItem(0).setText(1, "b")
    treeWidget.topLevelItem(0).setText(2, "c")
    treeWidget.topLevelItem(1).setText(0, "1")
    treeWidget.topLevelItem(1).setText(1, "2")
    treeWidget.topLevelItem(1).setText(2, "3")

    treeWidget.itemDropped.connect(buffer)
    treeWidget.model().rowsRemoved.connect(buffer)
    
    return treeWidget

def create_treeWidget_3():
    treeWidget=TreeWidget()
    treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
    treeWidget.setAlternatingRowColors(True)
    treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    treeWidget.setHeaderHidden(False)

    treeWidget.setDragEnabled(True)
    treeWidget.viewport().setAcceptDrops(True)
    treeWidget.setDropIndicatorShown(True)
    treeWidget.setDefaultDropAction(QtCore.Qt.MoveAction)
    treeWidget.setAcceptDrops(True)

    treeWidget.headerItem().setText(0, "Header 0")
    treeWidget.headerItem().setText(1, "Header 1")
    treeWidget.headerItem().setText(2, "Header 2")

    treeWidget.itemDropped.connect(buffer)
    treeWidget.model().rowsRemoved.connect(buffer)
    
    return treeWidget

def buffer():
    global counter
    counter+=1
    if counter==2: updateView(); counter=0        
    
def updateView():
    print('update')
    print('    update left:')
            
    iterator=QtWidgets.QTreeWidgetItemIterator(tree2)

    while iterator.value():
        item=iterator.value()
        for column in range(item.columnCount()): print('        '+item.text(column))
        print()
        iterator+=1
            
    print('\n    update right:')
    iterator=QtWidgets.QTreeWidgetItemIterator(tree3)

    while iterator.value():
        item=iterator.value()
        for column in range(item.columnCount()): print('        '+item.text(column))
        print()
        iterator+=1

    print()

    print('-----')

if __name__ == "__main__":
    import sys

    try:
        app=QtWidgets.QApplication(sys.argv)
        app.setStyle("fusion")
        tree1=create_treeWidget_1()
        tree2=create_treeWidget_2()
        tree3=create_treeWidget_3()
        tree1.show()
        tree2.show()
        tree3.show()
        sys.exit(app.exec_())

    except BaseException as e: print(e)

If an item gets moved from one tree too another the signal flow is

  1. x.itemDropped.connect
  2. x.models().rowsRemoved.connect -> correct output

If an item gets moved inside a tree the signal flow is

  1. x.models().rowsRemoved.connect
  2. x.itemDropped.connect -> correct output

To be honest I do not really know why the signal flow is not the same for both actions. But the second signal always returns the correct item distribution.


Solution

  • Based on what you described you don't need to subclass. In Tree1 set the default drop action to Qt.MoveAction so it will move the item to the target instead of copy.

    def create_treeWidget_1():
        ...
        treeWidget.setDragEnabled(True)
        treeWidget.setDefaultDropAction(QtCore.Qt.MoveAction)
        ...
    

    And in Tree2 remove the line setting the drag drop mode to InternalMove, since that only allows drops from itself, and instead also set the default drop action to Qt.MoveAction to enable moving items within itself.

    def create_treeWidget_2():
        ...
        treeWidget.setDragEnabled(True)
        treeWidget.viewport().setAcceptDrops(True)
        treeWidget.setDropIndicatorShown(True)
        treeWidget.setDefaultDropAction(QtCore.Qt.MoveAction)
        ...
    

    Why the relevant drag/drop properties are needed or not needed for each tree widget:

    Tree1

    • setDragEnabled(True) is used so items can be dragged to Tree2.
    • setAcceptDrops(True) is NOT used so items cannot be added to or moved within the view.
    • setDefaultDropAction(Qt.MoveAction) is used so items dragged and dropped into Tree2 are actually moved instead of copied.

    Tree2

    • setDragEnabled(True) is used to enable dragging its items.
    • setAcceptDrops(True) is used to allow either internal or external items to be dropped within the view.
    • setDefaultDropAction(Qt.MoveAction) is used so internal items dropped within the view are moved instead of copied, mimicking QAbstractItemView.InternalMove but still allowing external drops.

    You can disable the Qt.ItemIsDropEnabled flag for the new dropped items to prevent creating child items from them. In this case you do need to use the subclass.

    class TreeWidget(QtWidgets.QTreeWidget):
    
        def dropEvent(self, event):
            super().dropEvent(event)
            if event.source() is not self:
                item = self.itemAt(event.pos())
                if self.dropIndicatorPosition() == self.BelowItem:
                    item = self.itemBelow(item)
                elif self.dropIndicatorPosition() == self.OnViewport:
                    item = self.topLevelItem(self.topLevelItemCount() - 1)
                item.setFlags(item.flags() & ~QtCore.Qt.ItemIsDropEnabled)
    

    And then use it for Tree2

    def create_treeWidget_2():
        treeWidget=TreeWidget()