Search code examples
pythonpyqtdrag-and-dropqtreewidget

PyQt item column lost if QTreeWidget is set to DragDrop mode


I'm creating a folder-tree like structure using QTreeWidget. Each item has 2 columns:

  1. folder name.
  2. folder id, which is set to hidden by tree.hideColumn(1).

The goal is to enable drag/drop within the QTreeWidget as well as across widgets, and that requires setting the tree.setDragDropMode(DragDrop). However, after changing the mode from InternalMove to DrapDrop, I found that the dragged QTreeWidgetItem only has its 1st column retained, the previously existing 2nd column is lost. If I query item.data(1,0) it gives None.

If I don't hide the 2nd column, it won't get lost during the drag.

I'm rather confused. Any help is appreciated.

Below is a working example. If you drag any item onto another, the console will print column counts 1. Same as renaming (double click) a dragged item.

import sys
from PyQt5.QtWidgets import QTreeWidget, QVBoxLayout,\
        QMainWindow, QWidget, QTreeWidgetItem, QApplication, QAbstractItemView
from PyQt5.QtCore import Qt

class MainWindow(QMainWindow):

    def __init__(self):
        super(self.__class__, self).__init__()
        frame=QWidget()
        self.setCentralWidget(frame)
        hl=QVBoxLayout()
        frame.setLayout(hl)

        self.tree=QTreeWidget(self)
        self.tree.setColumnCount(2)

        # if I don't hide 2nd column, it won't get lost during the drag.
        self.tree.hideColumn(1)
        self.tree.setDragEnabled(True)

        # InternalMove gives 2 columns: name and id.
        # DragDrop would only give the 1st column after a drag/drop

        #self.tree.setDragDropMode(QAbstractItemView.InternalMove)
        self.tree.setDragDropMode(QAbstractItemView.DragDrop)

        hl.addWidget(self.tree)

        # add treewidgetitems
        data=[['Folder 1', '1'],
              ['Folder 2', '2'],
              ['Folder 3', '3']
              ]
        for ii in range(3):
            item=QTreeWidgetItem(data[ii])
            self.tree.addTopLevelItem(item)

        self.tree.itemDoubleClicked.connect(self.rename)
        self.tree.itemChanged.connect(self.postRename, Qt.QueuedConnection)

        self.show()


    def rename(self):
        item=self.tree.selectedItems()
        if item:
            item=item[0]
            item.setFlags(item.flags() | Qt.ItemIsEditable)
            self.tree.scrollToItem(item)
            self.tree.editItem(item)

    def postRename(self,item,column):

        print('postRename: column counts', item.columnCount())
        text=item.data(0,0)
        itemid=item.data(1,0)
        print('postRename: item text=',text, 'item id', itemid)
        return


if __name__ == "__main__":
     app = QApplication(sys.argv)
     form = MainWindow()
     form.show()
     sys.exit(app.exec_())

Solution

  • In the startDrag method of the views, only the indexes returned by selectedIndexes() are used as a base, and selectedIndexes() only returns the selectionModel() indexes that are not hidden, so the hidden column does not send the information. So a workaround is that the selectedIndexes() method does not filter the hidden indexes:

    import sys
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class TreeWidget(QtWidgets.QTreeWidget):
        def selectedIndexes(self):
            return self.selectionModel().selectedIndexes()
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self):
            super(self.__class__, self).__init__()
    
            self.tree = TreeWidget(columnCount=2,
                dragDropMode=QtWidgets.QAbstractItemView.DragDrop,
                dragEnabled=True)
            self.tree.hideColumn(1)
    
            self.tree.itemDoubleClicked.connect(self.rename)
            self.tree.itemChanged.connect(self.postRename)
    
            # add treewidgetitems
            data=[['Folder 1', '1'],
                  ['Folder 2', '2'],
                  ['Folder 3', '3']
                  ]
            for d in data:
                item = QtWidgets.QTreeWidgetItem(d)
                self.tree.addTopLevelItem(item)
    
            frame = QtWidgets.QWidget()
            self.setCentralWidget(frame)
            hl = QtWidgets.QVBoxLayout(frame)
            hl.addWidget(self.tree)
    
        @QtCore.pyqtSlot()
        def rename(self):
            item=self.tree.selectedItems()
            if item:
                item=item[0]
                item.setFlags(item.flags() | Qt.ItemIsEditable)
                self.tree.scrollToItem(item)
                self.tree.editItem(item)
    
        @QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem, int)
        def postRename(self, item, column):
            print('postRename: column counts', item.columnCount())
            text = item.data(0,0)
            itemid = item.data(1,0)
            print('postRename: item text=',text, 'item id', itemid)
    
    
    if __name__ == "__main__":
         app = QtWidgets.QApplication(sys.argv)
         form = MainWindow()
         form.show()
         sys.exit(app.exec_())
    

    On the other hand if your goal of establishing column 1 is just to save a data, it is best to use the roles avoiding hiding the columns:

    import sys
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    IDRole = QtCore.Qt.UserRole + 1000
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self):
            super(self.__class__, self).__init__()
    
            self.tree = QtWidgets.QTreeWidget(columnCount=1,
                dragDropMode=QtWidgets.QAbstractItemView.DragDrop,
                dragEnabled=True)
            self.tree.hideColumn(1)
    
            self.tree.itemDoubleClicked.connect(self.rename)
            self.tree.itemChanged.connect(self.postRename)
    
            # add treewidgetitems
            data=[['Folder 1', '1'],
                  ['Folder 2', '2'],
                  ['Folder 3', '3']
                  ]
            for d in data:
                text, itemid = d
                item = QtWidgets.QTreeWidgetItem([text])
                item.setData(0, IDRole, itemid)
                self.tree.addTopLevelItem(item)
    
            frame = QtWidgets.QWidget()
            self.setCentralWidget(frame)
            hl = QtWidgets.QVBoxLayout(frame)
            hl.addWidget(self.tree)
    
        @QtCore.pyqtSlot()
        def rename(self):
            item=self.tree.selectedItems()
            if item:
                item=item[0]
                item.setFlags(item.flags() | Qt.ItemIsEditable)
                self.tree.scrollToItem(item)
                self.tree.editItem(item)
    
        @QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem, int)
        def postRename(self, item, column):
            print('postRename: column counts', item.columnCount())
            text = item.text(0)
            itemid = item.data(0, IDRole)
            print('postRename: item text=',text, 'item id', itemid)
    
    if __name__ == "__main__":
         app = QtWidgets.QApplication(sys.argv)
         form = MainWindow()
         form.show()
         sys.exit(app.exec_())