From 28201b60c37bd3053a8780c2a22c8bdada8c00b5 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 21 Mar 2022 23:31:14 +0200 Subject: [PATCH] - added an example on a drag & drop tree widget picked from StackOverflow --- Utils/tree_widgt.py | 390 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 Utils/tree_widgt.py diff --git a/Utils/tree_widgt.py b/Utils/tree_widgt.py new file mode 100644 index 00000000..5574ff58 --- /dev/null +++ b/Utils/tree_widgt.py @@ -0,0 +1,390 @@ +import sys +import os +from PyQt6 import QtGui, QtCore +from PyQt6.QtGui import * +from PyQt6.QtWidgets import * +from PyQt6.QtCore import * +from copy import deepcopy +import pickle +import typing + +""" +https://stackoverflow.com/questions/22020091/how-to-handle-drag-and-drop-properly-using-pyqt-qabstractitemmodel + +Here is a code I ended up after two days of TreeView/Model madness. The subject appeared to be much more broad +than I thought. +I barely can spend so much time creating a singe widget. +Anyway. +The drag-and-drop functionality of TreeView items has been enabled. +But other than few interesting printout there is not much there. +The double click on an item allows the user to enter a new item name which won't be picked up. +EDITED A DAY LATER WITH A REVISED CODE. +It is now by 90% functional tool. + +The user can manipulate the TreeView items by drag and dropping, creating/duplicating/deleting and renaming. +The TreeView items are representing the directories or folders in hierarchical fashion before they are created on +a drive by hitting 'Print' button (instead of os.makedirs() the tool still simply prints each directory as a string. +I would say I am pretty happy with the result. Thanks to hackyday and to everyone who responded and helped with my +questions. + +A few last wishes... +A wish number 01: + + I wish the PrintOut() method would use a more elegant smarter function to loop through the TreeView items to + build a dictionary that is being passed to make_dirs_from_dict() method. + +A wish number 02: + + I wish deleting the items would be more stable. By some unknown reason a tool crashes on third/fourth Delete button + clicks. So far, I was unable to trace the problem down. + +A wish number 03: 3. I wish everyone the best and thanks for your help : + +REPLY: +I am not totally sure what you are trying to achieve, but it sounds like you want to retrieve the dragged item in the +drop operation, and have double click save a new node name. + +Firstly, you need to save the dragged item into the mimeData. Currently, you are only saving the string 'mimeData', +which doesn't tell you much. The mimeType string that it is saved as (here I used 'bstream') can actually be anything. +As long as it matches what you use to retrieve the data, and is in the list returned by the mimeTypes method of the +model. + +To pass the object itself, you must first serialize it (you can convert your object to xml alternatively, +if that was something you are planning on doing), since it is not a standard type for mime data. +In order for the data you enter to be saved you must re-implement the setData method of the model and define +behaviour for EditRole. + +EDIT: +That is a lot of code you updated, but I will oblige on the points you highlighted. Avoid calling createIndex outside +of the model class. This is a protected method in Qt; Python doesn't enforce private/protected variables or methods, +but when using a library from another language that does, I try to respect the intended organization of the classes, + and access to them. + +The purpose of the model is to provide an interface to your data. You should access it using the index, data, +parent etc. public functions of the model. +o get the parent of a given index, use that index's (or the model's) parent function, +which will also return a QModelIndex. This way, you don't have to go through (or indeed know about) the internal +structure of the data. This is what I did in the deleteLevel method. + +From the qt docs: + + To ensure that the representation of the data is kept separate from the way it is accessed, the concept of a model + index is introduced. Each piece of information that can be obtained via a model is represented by a model index... + only the model needs to know how to obtain data, and the type of data managed by the model can be defined fairly + generally. + +Also, you can use recursion to simplify the print method. +""" + + +class TreeItem(object): + def __init__(self, name, parent=None): + self.name = str(name) + self.parent = parent + self.children = [] + self.setParent(parent) + + def setParent(self, parent): + if parent is not None: + self.parent = parent + self.parent.appendChild(self) + else: + self.parent = None + + def appendChild(self, child): + self.children.append(child) + + def childAtRow(self, row): + if len(self.children) > row: + return self.children[row] + + def rowOfChild(self, child): + for i, item in enumerate(self.children): + if item == child: + return i + return -1 + + def removeChild(self, row): + value = self.children[row] + self.children.remove(value) + return True + + def __len__(self): + return len(self.children) + + +class TreeModel(QtCore.QAbstractItemModel): + def __init__(self): + + QtCore.QAbstractItemModel.__init__(self) + + self.columns = 1 + self.clickedItem = None + + self.root = TreeItem('root', None) + # each instance of TreeItem adds himself as a child to its parent + levelA = TreeItem('levelA', self.root) + levelB = TreeItem('levelB', levelA) + levelC1 = TreeItem('levelC1', levelB) + levelC2 = TreeItem('levelC2', levelB) + levelC3 = TreeItem('levelC3', levelB) + levelD = TreeItem('levelD', levelC3) + + levelE = TreeItem('levelE', levelD) + levelF = TreeItem('levelF', levelE) + + def nodeFromIndex(self, index): + return index.internalPointer() if index.isValid() else self.root + + def index(self, row: int, column: int, parent: QModelIndex = ...) -> QModelIndex: + node = self.nodeFromIndex(parent) + return self.createIndex(row, column, node.childAtRow(row)) + + def parent(self, child): + # print '\n parent(child)', child # PyQt4.QtCore.QModelIndex + if not child.isValid(): + return QModelIndex() + node = self.nodeFromIndex(child) + if node is None: + return QModelIndex() + parent = node.parent + if parent is None: + return QModelIndex() + grandparent = parent.parent + + if grandparent is None: + return QModelIndex() + + row = grandparent.rowOfChild(parent) + assert row != - 1 + + return self.createIndex(row, 0, parent) + + def rowCount(self, parent: QModelIndex = ...) -> int: + node = self.nodeFromIndex(parent) + if node is None: + return 0 + return len(node) + + def columnCount(self, parent: QModelIndex = ...) -> int: + return self.columns + + def data(self, index: QModelIndex, role: int = ...) -> typing.Any: + if role == Qt.ItemDataRole.DecorationRole: + return QVariant() + if role == Qt.ItemDataRole.TextAlignmentRole: + return QVariant(int(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)) + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + node = self.nodeFromIndex(index) + if index.column() == 0: + return QVariant(node.name) + elif index.column() == 1: + return QVariant(node.state) + elif index.column() == 2: + return QVariant(node.description) + else: + return QVariant() + + def supportedDropActions(self): + return Qt.DropAction.CopyAction | Qt.DropAction.MoveAction + + def flags(self, index): + defaultFlags = QAbstractItemModel.flags(self, index) + if index.isValid(): + return (Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsDragEnabled | + Qt.ItemFlag.ItemIsDropEnabled | defaultFlags) + else: + return Qt.ItemIsDropEnabled | defaultFlags + + def setData(self, index: QModelIndex, value: typing.Any, role: int = ...) -> bool: + if role == Qt.EditRole: + if value.toString() and len(value.toString()) > 0: + self.nodeFromIndex(index).name = value.toString() + self.dataChanged.emit(index, index) + return True + + def mimeTypes(self): + return ['bstream', 'text/xml'] + + def mimeData(self, indexes): + mimedata = QMimeData() + bstream = pickle.dumps(self.nodeFromIndex(indexes[0])) + mimedata.setData('bstream', bstream) + return mimedata + + def dropMimeData(self, mimedata, action, row, column, parentIndex): + if action == Qt.DropAction.IgnoreAction: + return True + + droppedNode = pickle.loads(mimedata.data('bstream')) + droppedIndex = self.createIndex(row, column, droppedNode) + + parentNode = self.nodeFromIndex(parentIndex) + + newNode = deepcopy(droppedNode) + newNode.setParent(parentNode) + + self.insertRow(len(parentNode)-1, parentIndex) + # self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex) + self.dataChanged.emit(parentIndex, parentIndex) + + return True + + def insertRow(self, row: int, parent: QModelIndex = ...) -> bool: + return self.insertRows(row, 1, parent) + + def insertRows(self, row: int, count: int, parent: QModelIndex = ...) -> bool: + self.beginInsertRows(parent, row, (row + (count - 1))) + self.endInsertRows() + return True + + def removeRow(self, row: int, parent: QModelIndex = ...) -> bool: + ret = self.removeRows(row, 1, parent) + return ret + + def removeRows(self, row: int, count: int, parent: QModelIndex = ...) -> bool: + self.beginRemoveRows(parent, row, row) + node = self.nodeFromIndex(parent) + node.removeChild(row) + self.endRemoveRows() + return True + + +class GUI(QDialog): + def __init__(self, parent=None, *args, **kwargs): + super().__init__(parent, *args, **kwargs) + self.myWidget = None + self.boxLayout = None + self.treeView = None + self.treeModel = None + self.PrintButton = None + self.DeleteButton = None + self.insertButton = None + self.duplicateButton = None + + def build(self, my_window): + my_window.resize(600, 400) + self.myWidget = QWidget(my_window) + self.boxLayout = QVBoxLayout(self.myWidget) + + self.treeView = QTreeView() + + self.treeModel = TreeModel() + self.treeView.setModel(self.treeModel) + self.treeView.expandAll() + self.treeView.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + # self.treeView.connect( + # self.treeView.model(), SIGNAL("dataChanged(QModelIndex,QModelIndex)"), self.onDataChanged) + self.treeView.model().dataChanged.connect(self.onDataChanged) + # QtCore.QObject.connect(self.treeView, QtCore.SIGNAL("clicked (QModelIndex)"), self.treeItemClicked) + self.treeView.clicked.connect(self.treeItemClicked) + self.boxLayout.addWidget(self.treeView) + + self.PrintButton = QPushButton("Print") + self.PrintButton.clicked.connect(self.printOut) + self.boxLayout.addWidget(self.PrintButton) + + self.DeleteButton = QPushButton("Delete") + self.DeleteButton.clicked.connect(self.deleteLevel) + self.boxLayout.addWidget(self.DeleteButton) + + self.insertButton = QPushButton("Insert") + self.insertButton.clicked.connect(self.insertLevel) + self.boxLayout.addWidget(self.insertButton) + + self.duplicateButton = QPushButton("Duplicate") + self.duplicateButton.clicked.connect(self.duplicateLevel) + self.boxLayout.addWidget(self.duplicateButton) + + my_window.setCentralWidget(self.myWidget) + + def make_dirs_from_dict(self, dirDict, current_dir='C:\\'): + for key, val in dirDict.items(): + os.mkdir(os.path.join(current_dir, key)) + print("Creating directory: ", os.path.join(current_dir, key)) + if type(val) == dict and val: + self.make_dirs_from_dict(val, os.path.join(current_dir, val)) + + def printOut(self): + result_dict = dictify(self.treeView.model().root) + self.make_dirs_from_dict(result_dict) + + def deleteLevel(self): + if len(self.treeView.selectedIndexes()) == 0: + return + + currentIndex = self.treeView.selectedIndexes()[0] + self.treeView.model().removeRow(currentIndex.row(), currentIndex.parent()) + + def insertLevel(self): + if len(self.treeView.selectedIndexes()) == 0: + return + + currentIndex = self.treeView.selectedIndexes()[0] + currentNode = currentIndex.internalPointer() + try: + newItem = TreeItem('Brand New', currentNode) + self.treeView.model().insertRow(len(currentNode)-1, currentIndex) + # self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), currentIndex, currentIndex) + except Exception as err: + print(err) + self.treeView.model().dataChanged.emit(currentIndex, currentIndex) + + def duplicateLevel(self): + if len(self.treeView.selectedIndexes()) == 0: + return + + currentIndex = self.treeView.selectedIndexes()[0] + currentRow = currentIndex.row() + currentColumn = currentIndex.column() + currentNode = currentIndex.internalPointer() + + parentNode = currentNode.parent + parentIndex = self.treeView.model().createIndex(currentRow, currentColumn, parentNode) + parentRow = parentIndex.row() + parentColumn = parentIndex.column() + + newNode = deepcopy(currentNode) + newNode.setParent(parentNode) + + self.treeView.model().insertRow(len(parentNode)-1, parentIndex) + # self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex) + self.treeView.model().dataChanged.emit(parentIndex, parentIndex) + + print( + '\n\t\t\t CurrentNode:', currentNode.name, ', ParentNode:', parentNode.name, ', currentColumn:', + currentColumn, ', currentRow:', currentRow, ', parentColumn:', parentColumn, ', parentRow:', parentRow + ) + self.treeView.update() + self.treeView.expandAll() + + def treeItemClicked(self, index): + print("\n clicked item ----------->", index.internalPointer().name) + + def onDataChanged(self, indexA, indexB): + print("\n onDataChanged NEVER TRIGGERED! ####################### \n ", indexA.internalPointer().name) + self.treeView.update(indexA) + self.treeView.expandAll() + # self.treeView.expanded() + + +def dictify(node): + kids = {} + try: + for child in node.children: + kids.update(dictify(child)) + except TypeError: + return {str(node.name): kids} + + +if __name__ == '__main__': + + app = QApplication(sys.argv) + + myWindow = QMainWindow() + myGui = GUI() + myGui.build(myWindow) + myWindow.show() + sys.exit(app.exec())