QTreeView drag/drop handling
-
-
OK, I went through imports and through the code and hopefully it's now very close to minimal.
from PyQt5 import QtCore, QtGui from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal, QModelIndex from PyQt5.QtWidgets import QWidget, QMainWindow, QDockWidget, QToolBar, QAction, QVBoxLayout, QSplitter, QSizePolicy, QTreeView, QTabWidget import PyQt5.QtWidgets from PyQt5.QtGui import QStandardItemModel, QStandardItem _DOCK_OPTS = PyQt5.QtWidgets.QMainWindow.AllowNestedDocks _DOCK_OPTS |= PyQt5.QtWidgets.QMainWindow.AllowTabbedDocks class TreeModel(QStandardItemModel): def __init__(self): QStandardItemModel.__init__(self) def supportedDropActions(self): return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction def mimeData(self, indexes): mimedata = QtCore.QMimeData() #mimedata.setData('text/xml', str(self.nodeFromIndex(indexes[0]))) #print("xxx mimeData = " + str(mimeData)) print("Starting mimeData....") print("Index: " + str(indexes)) print("------") print(str(self.nodeFromIndex(indexes[0]))) print("------2") node = self.nodeFromIndex(indexes[0]) print("Continuing mimeData....") #print("Node: " + str(node.data())) #mimedata.setData('text/xml', str(node)) #mimedata.setData('bstream', bstream) print("Continuing mimeData 2....") mimedata.setData('text/xml', 'mimeData') return mimedata def nodeFromIndex(self, index): print("Index2: " + str(index)) ip = None if index.isValid() == True: ip = index.internalPointer() print("isValid...") print(str(ip)) else: print(str(self.root)) ip = self.root print("before return") return ip ##return index.internalPointer() if index.isValid() else self.root def mimeTypes(self): return ['text/xml'] def dropMimeData(self, data, action, row, column, parent): print('dropMimeData %s %s %s %s' % (data.data('text/xml'), action, row, parent)) return True def flags(self, index): if not index.isValid(): return QtCore.Qt.ItemIsEnabled return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled # ----------------------------------------------------------------------------------------------------------------------------------- class Window(QMainWindow): selIdxsSignal = pyqtSignal(QModelIndex) def __init__(self): QMainWindow.__init__(self) self.setWindowTitle("Test") self.resize(600,500) self.tb = self.addToolBar("Test") self.ShowTitleBars = False self.setWidth = 1600 secondQMainWindow = QMainWindow() self.detailWidget = QWidget() self.detailLayout = QVBoxLayout() self.detailWidget.setLayout(self.detailLayout) self.detailData = ( # Database-like test data (1, -1, 20, 'GroupA', 'Group A' ,400, 0, '', False, 8, '', '', 0), (3, 1, 3, 'ItemA1', 'Item A1' , 50, 0, '', False, 8, '', '', 0), (4, 1, 1, 'ItemA2', 'Item A2' , 0, 0, '', True, 8, '', '', 0), (5, 1, 8, 'ItemA3', 'Item A3' , 90, 0, '', False, 8, '', '', 0), (6, 1, 10, 'ItemA4', 'Item A4' ,120, 0, '', False, 8, '', '', 0), (7, 1, 4, 'ItemA5', 'Item A5' , 70, 0, ' kg', True, 8, '', '', 2), (8, -1, 20, 'GroupB', 'Group B' , 0, 0, '', False, 8, '', '', 0), (9, 8, 7, 'ItemB1', 'Item B1' , 0, 0, '', True, 8, '', '', 0), (10, 8, 2, 'ItemB2', 'Item B2' , 0, 66, '', True, 8, '', '', 0), ) self.sourceData = ( # Database-like test data (1, -1, 1, 'Item1', 'Item 1', 0, 0, '', False, 8, '', '', 0), (2, -1, 2, 'Item2', 'Item 2', 0, 0, '', False, 8, '', '', 0), (3, -1, 3, 'Item3', 'Item 3', 0, 0, '', False, 8, '', '', 0), (4, -1, 4, 'Item4', 'Item 4', 0, 0, '', False, 8, '', '', 0), (5, -1, 5, 'Item5', 'Item 5', 0, 0, '', False, 8, '', '', 0), (6, -1, 6, 'Item6', 'Item 6', 0, 0, '', False, 8, '', '', 0), (7, -1, 7, 'Item7', 'Item 7', 0, 0, '', False, 8, '', '', 0), (8, -1, 8, 'Item8', 'Item 8', 0, 0, '', False, 8, '', '', 0), (9, -1, 9, 'Item9', 'Item 9', 0, 0, '', False, 8, '', '', 0), (10, -1, 10, 'Item10', 'Item 10', 0, 0, '', False, 8, '', '', 0), (11, -1, 11, 'Item11', 'Item 11', 0, 0, '', False, 8, '', '', 0), (12, -1, 12, 'Item12', 'Item 12', 0, 0, '', False, 8, '', '', 0), (20, -1, 20, 'Item13', 'Item 13', 0, 0, '', False, 8, '', '', 0), (21, -1, 21, 'Item14', 'Item 14', 0, 0, '', False, 8, '', '', 0), ) self.central = secondQMainWindow self.setDockOptions(_DOCK_OPTS) #.dockNestingEnabled(True) self.dw1 = QDockWidget("Pre-set items") self.dw1.topLevelChanged.connect(self.HideTitleBar) self.dw1.setTitleBarWidget(QWidget(self.dw1)) # Right treeview srcTreeView = QTreeView() srcTreeView.setIconSize(QSize(24,24)) srcTreeView.setHeaderHidden(True) srcTreeView.setDragEnabled(True) self.srcModel = TreeModel() srcroot = self.srcModel.invisibleRootItem() srcroot1 = QStandardItem("Group 1") srcroot.appendRow(srcroot1) srcroot2 = QStandardItem("Group 2") srcroot.appendRow(srcroot2) srcroot3 = QStandardItem("Group 3") srcroot.appendRow(srcroot3) for ID, ParentID, Type, Name, Label, Width, Height, Units, IsEditable, FontSize, FontVariant, NumberFormat, DecimalPlaces in self.sourceData: # for each record, generate an item in a treeModel, used in QTreeView print("ID = " + str(ID)) print("Name = " + Name) it = QStandardItem(Name) it.setData(ID) if ParentID == -1: srcroot1.appendRow(it) for item in self.iterItems(srcroot): #print(item.text()) if item.data() == ParentID: item.appendRow(it) srcTreeView.setModel(self.srcModel) self.dw1.setWidget(srcTreeView) srcTreeView.expandAll() srcTreeView.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding) self.addDockWidget(Qt.LeftDockWidgetArea, self.dw1) # Left treeview self.dw4 = QDockWidget("Group Editor") self.dw4.topLevelChanged.connect(self.HideTitleBar) self.dw4.setTitleBarWidget(QWidget(self.dw4)) DetailW = QWidget() DetailW.setAcceptDrops(True) DetailSplitter = QSplitter() self.DetailTree = QTreeView() self.DetailTree.setDragEnabled(True) self.treeModel = TreeModel() self.DetailTree.setHeaderHidden(True) self.DetailTree.setAcceptDrops(True) self.DetailTree.setIconSize(QSize(24,24)) DetailSplitter.addWidget(self.DetailTree) DetailSplitter.addWidget(self.detailWidget) self.dw4.setWidget(DetailSplitter) self.DetailTree.setObjectName = "DetailEditor" print("Check model: " + str(self.DetailTree.model)) root = self.treeModel.invisibleRootItem() root1 = QStandardItem("Master Group") root.appendRow(root1) for ID, ParentID, Type, Name, Label, Width, Height, Units, IsEditable, FontSize, FontVariant, NumberFormat, DecimalPlaces in self.detailData: # for each record, generate an item in a treeModel, used in QTreeView print("ID = " + str(ID)) print("Name = " + Name) it = QStandardItem(Name) it.setData(ID) if ParentID == -1: root1.appendRow(it) for item in self.iterItems(root): #print(item.text()) if item.data() == ParentID: item.appendRow(it) self.DetailTree.setModel(self.treeModel) self.DetailTree.expandAll() self.DetailTree.selectionModel().selectionChanged.connect(self.selIdxSignalChangeHandler) self.selIdxsSignal.connect(self.selectionChangeHandler) self.addDockWidget(Qt.RightDockWidgetArea, self.dw4) btn = QAction("A button",self) self.tb.addAction(btn) def selIdxSignalChangeHandler(self): self.selIdxsSignal.emit(self.DetailTree.selectionModel().selectedIndexes()[0]) def selectionChangeHandler(self, Idx): print(str(Idx)) IID = Idx.data(Qt.UserRole + 1) print(" Selection changed - IID = " + str(IID)) tbName = self.findChild(QLineEdit, "txtText") tbWidth = self.findChild(QLineEdit, "txtWidth") tbHeight = self.findChild(QLineEdit, "txtHeight") tbDataType = self.findChild(QLineEdit, "txtType") tbUnit = self.findChild(QLineEdit, "txtUnit") tbDecimals = self.findChild(QLineEdit, "txtDecimals") row = self.getRowByIndex(Idx) if IID != None: print("Assigning properties") #tbName.setText(str(row[3])) #tbWidth.setText(str(row[5])) #tbHeight.setText(str(row[6])) #tbDataType.setText(str(row[2])) #tbUnit.setText(str(row[7])) #tbDecimals.setText(str(row[12])) def getRowByIndex(self, Index): model = self.treeModel print("### getRowByIndex") SelID = model.data(Index, Qt.UserRole+1) print("### Selected ID = " + str(SelID)) row = None for i, ir in enumerate(self.detailData): if ir[0] == SelID: row = self.detailData[i] break return row def iterItems(self, root): # get all items, credits to @ekhumuro answer def recurse(parent): for row in range(parent.rowCount()): for column in range(parent.columnCount()): child = parent.child(row, column) yield child if child.hasChildren(): yield from recurse(child) if root is not None: yield from recurse(root) #def dropMimeData(self, mimedata, action, row, column, parentIndex): #if action == Qt.IgnoreAction: #return True #dragNode = mimedata.instance() #parentNode = self.nodeFromIndex(parentIndex) #newNode = deepcopy(dragNode) #<------ why copy? Why not just reparent? #newNode.setParent(parentNode) #self.insertRow(len(parentNode)-1, parentIndex) #self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex) #print("Done mime data") #return True def HideTitleBar(self): dockw = self.sender() if dockw.isFloating() == False and self.ShowTitleBars == False: dockw.setTitleBarWidget(QWidget(dockw)) # ---------------------------------------------------------------------------------------------------------------------------------------------------------------- if __name__ == '__main__': import sys app = PyQt5.QtWidgets.QApplication(sys.argv) window = Window() window.show() app.exec_()
-
@Oak77
So, just as an example, in order to reproduce your crash, it is necessary to have dock widgets, toolbars, tabs, menus, tool buttons, icons, pixmaps, palettes, colours, validators, and Python pickling? Then you must have one of the most complex situations/problems to diagnose that I have ever heard of in a Qt program, and it must be incredibly difficult to diagnose with all those interactions contributing. Best of luck, hopefully somebody will know why these contribute to the issue and give you the solution. -
@JonB said in QTreeView drag/drop handling:
@Oak77
So, just as an example, in order to reproduce your crash, it is necessary to have dock widgets, toolbars, tabs, menus, tool buttons, icons, pixmaps, palettes, colours, validators, and Python pickling?I don't get it. I removed all dock widgets except the two in which the QTabTreeView sits, I removed all tabs, all icons, all tool buttons except one empty dummy, all icons, all pixmap references, all palettes, all colours, pickling, menus,...
Did you look into the previous code?
I mean you're right I could remove the (now basically empty) toolbar altogether and the
QDockWidgets
containers in whichQTreeViews
sit, but that would strip only few lines of code and didn't seem to be important. That does not affect the drag and drop part, which is basically in separate class at the beginning of the code.I'm not saying I won't remove the remaining few extra objects, if you thing it's important. But from looking at your reply, it seems you just glanced at wrong code.
-
AFAIK, you need to reimplement dropMimeData and there you insert the dropped data the way you want/need in your model.
-
@SGaist Right, so I'm not doing that? Using:
def dropMimeData(self, data, action, row, column, parent): print('dropMimeData %s %s %s %s' % (data.data('text/xml'), action, row, parent)) return True
That's only printing data, I know. But if I understand it correctly, it's dependant on what's inside the
mimeData
and that in turn on reimplementedmimeData
...(?). And that's where I'm stuck, perhaps because the selection model is so far beyond my full understanding.
What I'm trying to do is to retrieve theindex
, out of indexed row thedata()
, which should be anID
and get a datarow by thatID
. Something I would do in VB.net using aValueMember
and thus it would be a trivial task. It's well possible I'm doing it wrong way, but I didn't find any usable examples for the job.
The code itself crashes on an attempt to use (or return) ofindex.internalPointer()
. I took the original code from some examples, but they were used inQAbsstractModel
, while I'm usingQStandardItemModel
. I'm quite lost as for how to debug this.By the way, if anyone knows of a good, complete example that covers this (QTreeVeiw, QStandardItemModel /or feasible alternative/, items with value member), please just point me in that direction, I'm willing to start over :-).
-
I think that my problem comes from the fact, that I am following (the only available AFAIK) examples made in PyQt4 (and Python 2.6 I guess, looking at print syntaxe).
Here's one taken from QTreeView with drag and drop support in PyQt
, which I converted (or attemted, at least) to PyQt5 and Python 3.6:import sys from PyQt5 import QtGui, QtCore from PyQt5.QtWidgets import QMainWindow, QApplication, QTreeView, QAbstractItemView class TreeModel(QtCore.QAbstractItemModel): def __init__(self): QtCore.QAbstractItemModel.__init__(self) self.nodes = ['node0', 'node1', 'node2'] def index(self, row, column, parent): return self.createIndex(row, column, self.nodes[row]) def parent(self, index): return QtCore.QModelIndex() def rowCount(self, index): if index.internalPointer() in self.nodes: return 0 return len(self.nodes) def columnCount(self, index): return 1 def data(self, index, role): if role == 0: return index.internalPointer() else: return None def supportedDropActions(self): return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction def flags(self, index): if not index.isValid(): return QtCore.Qt.ItemIsEnabled return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | \ QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled def mimeTypes(self): return ['text/xml'] def mimeData(self, indexes): mimedata = QtCore.QMimeData() mimedata.setData('text/xml', 'mimeData') return mimedata def dropMimeData(self, data, action, row, column, parent): print('dropMimeData %s %s %s %s' % (data.data('text/xml'), action, row, parent)) return True class MainForm(QMainWindow): def __init__(self, parent=None): super(MainForm, self).__init__(parent) self.treeModel = TreeModel() self.view = QTreeView() self.view.setModel(self.treeModel) self.view.setDragDropMode(QAbstractItemView.InternalMove) self.setCentralWidget(self.view) def main(): app = QApplication(sys.argv) form = MainForm() form.show() app.exec_() if __name__ == '__main__': main()
It doesn't work either, with error:
Traceback (most recent call last): File "tree_sample9.py", line 44, in mimeData mimedata.setData('text/xml', 'mimeData') TypeError: setData(self, str, Union[QByteArray, bytes, bytearray]): argument 2 has unexpected type 'str'
I tried couple of different examples and got a result like that in all cases. I know it might be me messing something up the conversion, but I have no idea what's wrong.
Could anyone advice please, what could be wrong? And/or perhaps someone knows a good working example for Drag&Drop support in PyQt5? -
@Oak77 said in QTreeView drag/drop handling:
mimedata.setData('text/xml', 'mimeData') TypeError: setData(self, str, Union[QByteArray, bytes, bytearray]): argument 2 has unexpected type 'str'
One of (something like) the following:
mimedata.setData('text/xml', QtCore.QByteArray('mimeData')) mimedata.setData('text/xml', QtCore.QString('mimeData').toUtf8()) mimedata.setData('text/xml', 'mimeData'.encode())
-
@JonB said in QTreeView drag/drop handling:
One of (something like) the following:
mimedata.setData('text/xml', QtCore.QByteArray('mimeData')) mimedata.setData('text/xml', QtCore.QString('mimeData').toUtf8()) mimedata.setData('text/xml', 'mimeData'.encode())
Thank you very much! The very last option worked. I'm guessing I'm encoding the string into default UTF-8, but have no idea why it doesn't work without it, however now I can progress a bit and study this stuff...
Thank you again, that helped a lot! -
'This is a string' b'This is not a string'
Python3 makes a strict distinction between bytes and string.
-
@SGaist said in QTreeView drag/drop handling:
'This is a string' b'This is not a string'
Python3 makes a strict distinction between bytes and string.
Right, but...:
'mimeData is a string'.encode()
...is a string in UTF-8, correct? What I was confused about is, that string (which I'd assume is UTF-8 by default) wouldn't work until it's encoded by
encode()
method, which with no argument, turns the string into UTF-8 string. I don't get why it crashed before and it's fine after encoding, especially because apparently it would work in PyQt4 (I don't have PyQt4 installed, didn't check, but all the references are with plain string). Not a big deal though, just my curiosity why. -
@Oak77 said in QTreeView drag/drop handling:
I don't get why it crashed before and it's fine after encoding,
The error message told you that you were passing a
str
/QString
and the method expects aQByteArray
/bytes
, as per the documentation. They are not the same. I can't comment about PyQt4 --- you'd have to look up changes, and Qt4/PyQt4 has changed at 5, and also Python from 2.x to 3.x if relevant --- but it's just a parameter type mismatch here, no mystery. (BTW, yes,encode()
is UTF-8 default.) -
The encode method returns a bytes object.
-
Thank you for your explanation, both thanks to your comments and my 2 hours over docs and testing yesterday helped me. You might hate me though, through all that time I was unable to convert that QByteArray back to string :-). It makes me feel stupid, but looking into tons of confused threads, at least I'm not alone...
b = b'MyString'
...is a QByteArray, if I'm not completely lost. Then...
print(b.decode("utf-8"))
...should output (and does):
MyString
Then, when the same
MyString
string is set todata
, we get:print(" 1 ---- " + str(data.data('text/xml')))
outputs a string:
1 ---- b'MyString'
And I would expect decoding it would give me a string:
print(" 2 ---- " + str(data.data('text/xml').decode("utf-8")))
...but, I get an error:
Traceback (most recent call last): File "test4.py", line 35, in dropMimeData print(" 2 ---- " + str(data.data('text/xml').decode("utf-8")))
OK, there's a brute, ugly way, but I don't anyone expect to embrace it:
print(str(data.data('text/xml'))[2:-1])
Output is correct:
MyString
I did google related threads, but none related to mimeData and it works with byte arrays as shown above. Maybe I'm getting old for this stuff :-)
-
@Oak77 said in QTreeView drag/drop handling:
b = b'MyString'
...is a QByteArray, if I'm not completely lost.
Do not use single letter variable. It's a easy way to break all kinds of stuff like the interaction in the debugger.
That said, sorry, you are a bit lost. It's a Python bytes. The method you are calling accepts it as well as QByteArray that you would have to explicitly build. You cannot convert a bytes to a string by calling str() on it, you have to use the encode method. If you call str(your_bytes) you will get "b'MyString'".