0

I'm trying to implement drag-and-drop functionality in a QTreeView widget in PyQt6. I have enabled drag-and-drop support using setDragEnabled(True), setAcceptDrops(True), and setDropIndicatorShown(True) to show the drop indicator when dragging items.

However, after overriding the built-in drag and drop event functions (dragEnterEvent, dragMoveEvent, and dropEvent), the drop indicator (the line between rows where the dragged item can be dropped) no longer appears.

How can I restore or properly display the drop indicator between items after overriding the drag-and-drop event functions in PyQt6's QTreeView?

Here is the code I have so far:

from PyQt6 import QtCore, QtGui, QtWidgets

class ObjectTree(QtWidgets.QTreeView):
    def __init__(self):
        super().__init__()

        self.model = QtGui.QStandardItemModel()
        self.setModel(self.model)

        # Enable drag and drop
        self.setDragEnabled(True)
        self.setAcceptDrops(True)
        self.setDropIndicatorShown(True)  # Show drop indicator

        self._populate_tree()

    def _populate_tree(self):
        root_item = QtGui.QStandardItem('Root')
        self.model.appendRow(root_item)
        for i in range(10):
            child_item = QtGui.QStandardItem(f'Child {i + 1}')
            root_item.appendRow(child_item)

    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat('application/x-item'):
            event.acceptProposedAction()
        else:
            event.ignore()

    def dragMoveEvent(self, event):
        if event.mimeData().hasFormat('application/x-item'):
            event.acceptProposedAction()
        else:
            event.ignore()

    def dropEvent(self, event):
        if event.mimeData().hasFormat('application/x-item'):
            data = event.mimeData().data('application/x-item')
            stream = QtCore.QDataStream(data, QtCore.QIODevice.OpenModeFlag.ReadOnly)
            item_name = stream.readQString()
            print(f"Dropped item: {item_name}")
            event.acceptProposedAction()

    def startDrag(self, event):
        selected_indexes = self.selectedIndexes()
        if selected_indexes:
            selected_item = self.model.itemFromIndex(selected_indexes[0])
            mime_data = QtCore.QMimeData()
            data = QtCore.QByteArray()
            stream = QtCore.QDataStream(data, QtCore.QDataStream.OpenModeFlag.WriteOnly)
            stream.writeQString(selected_item.text())
            mime_data.setData('application/x-item', data)

            drag = QtGui.QDrag(self)
            drag.setMimeData(mime_data)
            drag.exec(event)

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.object_tree = ObjectTree()
        self.setCentralWidget(self.object_tree)

        self.setWindowTitle("PyQt6 Drag and Drop Example")
        self.setGeometry(100, 100, 600, 400)

if __name__ == "__main__":
    app = QtWidgets.QApplication([])  
    window = MainWindow()  
    window.show()  
    app.exec()  
2
  • 1
    By overriding the drag/drop event handlers, you're also overriding the default behavior, which is also to properly update the drop indicator position. You should call the default implementation first, and then add your own code, at least for dragMoveEvent(). Note that in a case like your is usually more appropriate to subclass the model instead and override the related methods: mimeTypes() (to add your own type), mimeData() (to write the related data when dragging) and dropMimeData (to eventually do something when dropping). The view will then conform to that automatically. Commented Dec 30, 2024 at 21:56
  • Thanks! The second approach worked perfectly for me. Commented Jan 1 at 12:30

1 Answer 1

1

Based on the suggestion by @musicamante, I've updated the code to use model subclassing for drag and drop functionality. This approach correctly displays the drop indicator and provides a cleaner implementation.

Here's the corrected code:

from PyQt6 import QtCore, QtGui, QtWidgets

class ObjectTreeModel(QtGui.QStandardItemModel):
    def __init__(self):
        super().__init__()

    def mimeTypes(self):
        return ['application/x-item']

    def mimeData(self, indexes):
        if not indexes:
            return None
        item = self.itemFromIndex(indexes[0])
        mime_data = QtCore.QMimeData()
        data = QtCore.QByteArray()
        stream = QtCore.QDataStream(data, QtCore.QDataStream.OpenModeFlag.WriteOnly)
        stream.writeQString(item.text())
        mime_data.setData('application/x-item', data)
        return mime_data

    def dropMimeData(self, data, action, row, column, parent):
        if not data.hasFormat('application/x-item'):
            return False

        stream = QtCore.QDataStream(data.data('application/x-item'), QtCore.QIODevice.OpenModeFlag.ReadOnly)
        item_name = stream.readQString()
        print(f"Dropped item: {item_name}")

        if parent and parent.isValid():
            parent_item = self.itemFromIndex(parent)
            new_item = QtGui.QStandardItem(item_name)
            parent_item.insertRow(row,new_item)
        else:
            new_item = QtGui.QStandardItem(item_name)
            self.insertRow(row, new_item)
        return True

    def flags(self, index):
         default_flags = super().flags(index)
         if index.isValid():
            return default_flags | QtCore.Qt.ItemFlag.ItemIsDragEnabled | QtCore.Qt.ItemFlag.ItemIsDropEnabled
         else:
             return default_flags | QtCore.Qt.ItemFlag.ItemIsDropEnabled


class ObjectTree(QtWidgets.QTreeView):
    def __init__(self):
        super().__init__()

        self.model = ObjectTreeModel()
        self.setModel(self.model)

        # Enable drag and drop
        self.setDragEnabled(True)
        self.setAcceptDrops(True)
        self.setDropIndicatorShown(True)

        self._populate_tree()

    def _populate_tree(self):
        root_item = QtGui.QStandardItem('Root')
        self.model.appendRow(root_item)
        for i in range(10):
            child_item = QtGui.QStandardItem(f'Child {i + 1}')
            root_item.appendRow(child_item)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.object_tree = ObjectTree()
        self.setCentralWidget(self.object_tree)

        self.setWindowTitle("PyQt6 Drag and Drop Example")
        self.setGeometry(100, 100, 600, 400)

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

This updated code moves the drag and drop logic to a custom ObjectTreeModel which inherits from QtGui.QStandardItemModel. This approach allows the QTreeView to properly handle the display of the drop indicator.

Key changes:

  • The ObjectTreeModel now handles the mimeTypes(), mimeData(), and dropMimeData() methods.

  • The flags() method is updated to enable dragging and dropping on the items.

  • The ObjectTree class now uses an instance of ObjectTreeModel.

  • Removed the overridden event handlers from the ObjectTree class

By using this approach, we can leverage the default behavior of QTreeView and correctly display the drop indicator.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.