Implement Drag and Drop in a QTreeView to reorder items like a window file explorer

82 views Asked by At

it's been weeks that I am trying to implement Drag and Drop in my QTreeView. So i decided to create this post to find a solution. I searched on many websites, on the Qt's official website, and on github but each time it doesn't work. I also tried to ask chatGPT if it can show me an exemple of drag and drop in a QTreeView but I didn't work either. So here is what I am trying to achieve : I want to create a QTreeView from the QAbstractItemModel class. Then I would like to be able to reorder my items for exemple moving the last item to the second row, or move it on an other item in order to insert it as a children of this item. It would be nice if I also could move a child in order to make a top level item. By the way, I am using PySide6 so maybe the problem comes from the module ? Here is a piece of code to work on, it already has a model with children so you can test every methods in it and add those you need to implement drag and drop :

import sys

from PySide6.QtGui import *
from PySide6.QtCore import *
from PySide6.QtWidgets import *




class MainWindow(QMainWindow):
    def __init__(self, parent: QWidget = None):
        super().__init__(parent)
        self.resize(573, 468)

        self.frame = QFrame()
        self.setCentralWidget(self.frame)
        self.hlayout = QHBoxLayout()
        self.frame.setLayout(self.hlayout)


        self.view = QTreeView()
        self.view.setAlternatingRowColors(True)
        self.view.setSelectionBehavior(QAbstractItemView.SelectItems)
        self.view.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.view.setAnimated(False)
        self.view.setAllColumnsShowFocus(True)
        self.hlayout.addWidget(self.view)
        headers = ["value",'Type','Progress Bar','Test','Test','Test','Test']


        self.model = TreeModel(headers, self)
        # self.proxyModel = QSortFilterProxyModel(self)
        # self.proxyModel.setSourceModel(self.model)
        self.view.setModel(self.model)

        self.view.expandAll()
        for column in range(self.model.columnCount()):
            self.view.resizeColumnToContents(column)



class TreeItem:
    def __init__(self, data: list, parent: 'TreeItem' = None):
        self.item_data = data
        self.parent_item = parent
        self.child_items = []

    def child(self, number: int) -> 'TreeItem':
        if number < 0 or number >= len(self.child_items):
            return None
        return self.child_items[number]

    def last_child(self):
        return self.child_items[-1] if self.child_items else None

    def child_count(self) -> int:
        return len(self.child_items)

    def child_number(self) -> int:
        if self.parent_item:
            return self.parent_item.child_items.index(self)
        return 0

    def column_count(self) -> int:
        return len(self.item_data)

    def data(self, column: int):
        if column < 0 or column >= len(self.item_data):
            return None
        return self.item_data[column]

    def insert_children(self, position: int, count: int, columns: int) -> bool:
        if position < 0 or position > len(self.child_items):
            return False

        for row in range(count):
            data = [None] * columns
            item = TreeItem(data.copy(), self)
            self.child_items.insert(position, item)

        return True
    
    def remove_children(self, position: int, count: int) -> bool:
        if position < 0 or position + count > len(self.child_items):
            return False

        for row in range(count):
            self.child_items.pop(position)

        return True


    def parent(self):
        return self.parent_item


    def set_data(self, column: int, value):
        if column < 0 or column >= len(self.item_data):
            return False

        self.item_data[column] = value
        return True

    def __repr__(self) -> str:
        result = f"<treeitem.TreeItem at 0x{id(self):x}"
        for d in self.item_data:
            result += f' "{d}"' if d else " <None>"
        result += f", {len(self.child_items)} children>"
        return result




class TreeModel(QAbstractItemModel):

    def __init__(self, headers: list, parent=None):
        super().__init__(parent)

        self.root_data = headers
        self.root_item = TreeItem(self.root_data.copy())
        self.setup_model_data(self.root_item)

    def columnCount(self, parent: QModelIndex = None) -> int:
        return self.root_item.column_count()

    def data(self, index: QModelIndex, role: int = None):
        if not index.isValid():
            return None

        if role != Qt.DisplayRole and role != Qt.EditRole:
            return None

        item: TreeItem = self.get_item(index)

        return item.data(index.column())

    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        if not index.isValid():
            return Qt.NoItemFlags

        return Qt.ItemIsEditable | QAbstractItemModel.flags(self, index)

    def get_item(self, index: QModelIndex = QModelIndex()) -> TreeItem:
        if index.isValid():
            item: TreeItem = index.internalPointer()
            if item:
                return item

        return self.root_item
      
    def headerData(self, section: int, orientation: Qt.Orientation,
                   role: int = Qt.DisplayRole):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.root_item.data(section)

        return None

    def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:

        if parent.isValid() and parent.column() != 0:
            return QModelIndex()

        parent_item: TreeItem = self.get_item(parent)
        if not parent_item:
            return QModelIndex()
        

        child_item: TreeItem = parent_item.child(row)
        if child_item:
            return self.createIndex(row, column, child_item)
        return QModelIndex()

    def insertColumns(self, position: int, columns: int,
                      parent: QModelIndex = QModelIndex()) -> bool:
        self.beginInsertColumns(parent, position, position + columns - 1)
        success: bool = self.root_item.insert_columns(position, columns)
        self.endInsertColumns()

        return success

    def insertRows(self, position: int, rows: int,
                   parent: QModelIndex = QModelIndex()) -> bool:
        parent_item: TreeItem = self.get_item(parent)
        if not parent_item:
            return False

        self.beginInsertRows(parent, position, position + rows - 1)
        column_count = self.root_item.column_count()
        success: bool = parent_item.insert_children(position, rows, column_count)
        self.endInsertRows()

        return success

    def parent(self, index: QModelIndex = QModelIndex()) -> QModelIndex:
        if not index.isValid():
            return QModelIndex()

        child_item: TreeItem = self.get_item(index)
        if child_item:
            parent_item: TreeItem = child_item.parent()
        else:
            parent_item = None

        if parent_item == self.root_item or not parent_item:
            return QModelIndex()

        return self.createIndex(parent_item.child_number(), 0, parent_item)

    def removeColumns(self, position: int, columns: int,
                      parent: QModelIndex = QModelIndex()) -> bool:
        self.beginRemoveColumns(parent, position, position + columns - 1)
        success: bool = self.root_item.remove_columns(position, columns)
        self.endRemoveColumns()

        if self.root_item.column_count() == 0:
            self.removeRows(0, self.rowCount())

        return success

    def removeRows(self, position: int, rows: int,
                   parent: QModelIndex = QModelIndex()) -> bool:
        parent_item: TreeItem = self.get_item(parent)
        if not parent_item:
            return False

        self.beginRemoveRows(parent, position, position + rows - 1)
        success: bool = parent_item.remove_children(position, rows)
        self.endRemoveRows()

        return success

    def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
        if parent.isValid() and parent.column() > 0:
            return 0

        parent_item: TreeItem = self.get_item(parent)
        if not parent_item:
            return 0
        return parent_item.child_count()

    def setData(self, index: QModelIndex, value, role: int) -> bool:
        if role != Qt.EditRole:
            return False

        item: TreeItem = self.get_item(index)
        result: bool = item.set_data(index.column(), value)

        if result:
            self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole])

        return result
    

    def setHeaderData(self, section: int, orientation: Qt.Orientation, value,
                      role: int = None) -> bool:
        if role != Qt.EditRole or orientation != Qt.Horizontal:
            return False

        result: bool = self.root_item.set_data(section, value)

        if result:
            self.headerDataChanged.emit(orientation, section, section)

        return result

    def setup_model_data(self, parent: TreeItem):
        parents = [parent]
        Data = [["Test 1",'diam',10,2,3,4,5,6],["Test 2",'len',55,2,3,4,5,6],["Test 3",'conc',64,2,3,4,5,6],["Test 4",'diam',9,2,3,4,5,6],["Test 5",'len',12,2,3,4,5,6]]
        for item in Data :
            parent : TreeItem = parents[0]
            col_count = self.root_item.column_count()
            parent.insert_children(parent.child_count(), 1, col_count)
            column_data = [item[0],item[1],item[2],item[3],item[4],item[5],item[6]]

            for column in range(len(column_data)):
                child = parent.last_child()
                child.set_data(column, column_data[column])

            parent2 : TreeItem = parent.last_child()
            parent2.insert_children(parent2.child_count(), 1, col_count)
            column_data = [item[0],item[1],item[2],item[3],item[4],item[5],item[6]]

            for column in range(len(column_data)):
                child = parent2.last_child()
                child.set_data(column, column_data[column])
        


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

Every try that I gave crashed with each time different error messages. Most of the time, I can only drop items on other but I can never insert them in the middle of others. So I think that starting back from scratch is the best solution to understand how it works and how to implement it correctly. I really hope that someone will find an answer and I thank you for taking the time to read this post.

0

There are 0 answers