QTreeView drag/drop handling
-
I have some obvious questions for which I wasn't able to find satisfactory answers.
- Is there a way (subclass, etc.) to have access to a kind of drag/drop event, so that we could control a dropped item?
I.e. in the example, if we create a copy of item by dragging, it's
QStandardItemModel
row gets copied, so it gets the sameID
andName
. That is obviously unacceptable,ID
should be a new one (typically obtained from a database) andName
should be somtehing like "OriginalName_1".- Is there a way to control, whether an item can be created on drop or not?
I.e. Toronto shouldn't be allowed to be dropped into United States, but let's pretend it can be dropped to another Canadian state. We need to know where it is being dropped and then have a way to create a record and tree item or do nothing (based on custom function evaluating the items).
- Is there a way to set
QTreeView
in a way, that drag&drops within aQTreeView
would move items (they're copied by default), while dropping from anotherQTreeView
would copy an item (that works)?
I found few threads on this subject, but they don't seem to answer the question and got me perhaps even more confused.
Here is a working sample code I use for testing.
import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QTreeView, QTreeWidgetItemIterator, QAction from PyQt5.Qt import QStandardItemModel, QStandardItem from PyQt5.QtGui import QFont, QColor class StandardItem(QStandardItem): def __init__(self, txt='', font_size=12, set_bold=False, color=QColor(0, 0, 0)): super().__init__() fnt = QFont('Open Sans', font_size) fnt.setBold(set_bold) self.setEditable(False) self.setForeground(color) self.setFont(fnt) self.setText(txt) class AppDemo(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle('World Country Diagram') self.resize(500, 700) self.tb = self.addToolBar("TestBar") LstTr = QAction("List tree",self) LstTr.triggered.connect(self.ListTree) self.tb.addAction(LstTr) treeView = QTreeView() treeView.setHeaderHidden(True) treeView.setDragEnabled(True) treeView.setAcceptDrops(True) self.treeModel = QStandardItemModel() rootNode = self.treeModel.invisibleRootItem() Data = ( # Database-like test data #ID Parent ID Type Name Prop1 #--------------------------------------------------- ---------- (1, -1, 1, 'United States', 'USA' ), (3, 1, 2, 'California', 'CA' ), (4, 3, 3, 'Oakland', '-' ), (5, 3, 3, 'San Francisco', '-' ), (6, 3, 3, 'San Jose', '-' ), (7, 1, 2, 'Texas', 'TX' ), (8, 7, 3, 'Austin', '-' ), (9, 7, 3, 'Houston', '-' ), (10, 7, 3, 'Dallas', '-' ), (11, -1, 1, 'Canada', 'CAN' ), (12, 11, 2, 'Alberta', '' ), (13, 11, 2, 'British Columbia', '' ), (14, 11, 2, 'Ontario', '' ), (15, 14, 3, 'Toronto', '-' ), (16, 13, 3, 'Vancouver', '-' ), (17, 12, 3, 'Calgary', '-' ), (18, 13, 3, 'Wells', '-' ) ) root = self.treeModel.invisibleRootItem() for ID, ParentID, Type, Name, Prop1 in Data: # 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) #, Name) if ParentID == -1: self.treeModel.appendRow(it) for item in self.iterItems(root): #print(item.text()) if item.data() == ParentID: item.appendRow(it) treeView.setModel(self.treeModel) treeView.expandAll() treeView.doubleClicked.connect(self.getValue) self.treeModel.itemChanged.connect(self.ModelItemChanged) self.setCentralWidget(treeView) def getValue(self, val): print(val.data()) print(val.row()) print(val.column()) print("-------------") #self.listTree() def ModelItemChanged(self, val): print("Model item changed") print(val.data()) print(val.row()) print(val.column()) print("-------------") def listTree(self): for it in self.treeModel: print(str(it)) 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 ListTree(self): print("================================================================") root = self.treeModel.invisibleRootItem() for item in self.iterItems(root): print(item.text() + " " + str(item.data()) ) print("================================================================") app = QApplication(sys.argv) demo = AppDemo() demo.show() sys.exit(app.exec_())
Double click prints current item. On drop a
ItemChanged
is fired and printed a message. A button "ListTree" lists alltreeModel
rows - comparing before and after shows how a new row was added.I took this tutorial and heavily modified, with my adjustments, some mods were based on @ekhumuro SO answers. Credits to those authors.
-
Hi,
Did you already read the Using Drag & Drop with item views chapter in Qt's documentation ?
-
@SGaist Well, to be honest I probably didn't. I went through documentation, but I'd start at places like
QTreeView
orQStandardItemModel
, basically stuff I can see employed in the code.I read it now and I can see some helpful sections, but as soon as I started to test them I hit walls. I will try to search for different references to find a way out by myself first, though. Thank you for pointing that piece of documentation out.
-
Basically months (of some spare time) into the research and testing, I'm still unable to do this simple task. The documentation and threads are fragmented between different ways of doing the same thing and I basically found none of them completely describing this very basic stuff.
I am including my latest test file, where I have an issue I've been fighting for couple of last weeks. Currently it crashes every time I try to use
internalPointer()
ofisValid() == true
index.from PyQt5 import QtCore, QtGui from PyQt5.QtCore import Qt, QSize, QAbstractItemModel, pyqtSlot, pyqtSignal, QModelIndex from PyQt5.QtWidgets import QWidget, QMainWindow, QTextEdit, QDockWidget, QToolBar, QTabWidget, QMenu, QAction, QLayout, QTabBar, QMenu, QToolButton, QTableView, QHeaderView, QHBoxLayout, QVBoxLayout, QGridLayout, QLabel, QSplitter, QLineEdit, QSpacerItem, QSizePolicy, QPushButton, QComboBox, QDateEdit, QTreeWidget, QTreeWidgetItem, QGroupBox, QTreeView, QSpinBox, QCheckBox, QDoubleSpinBox, QRadioButton, QDateEdit, QTimeEdit, QDateTimeEdit import PyQt5.QtWidgets from PyQt5.QtGui import QIcon, QPixmap, QStandardItemModel, QStandardItem, QPalette, QColor, QIntValidator, QDoubleValidator import _pickle as cPickle _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)) #bstream = cPickle.dumps(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 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 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 8, '', '', 0), (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.setTabPosition (Qt.LeftDockWidgetArea, QTabWidget.North) 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)) srcTreeView = QTreeView() srcTreeView.setIconSize(QSize(24,24)) srcTreeView.setHeaderHidden(True) srcTreeView.setDragEnabled(True) self.srcModel = TreeModel() srcroot = self.srcModel.invisibleRootItem() srcroot1 = QStandardItem("Group 1") srcroot1.setIcon(QIcon(QPixmap('detail.png'))) srcroot.appendRow(srcroot1) srcroot2 = QStandardItem("Group 2") srcroot2.setIcon(QIcon(QPixmap('detail.png'))) srcroot.appendRow(srcroot2) srcroot3 = QStandardItem("Group 3") srcroot3.setIcon(QIcon(QPixmap('detail.png'))) 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) #, Name) it.setIcon(QIcon(self.getIconByType(Type))) 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) SourceTree = QTreeView() self.addDockWidget(Qt.LeftDockWidgetArea, self.dw1) 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") root1.setIcon(QIcon(QPixmap('detail.png'))) 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) #, Name) it.setIcon(QIcon(self.getIconByType(Type))) 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) DetailGB1 = QGroupBox() DetailGB1.setMaximumWidth(300) self.addDockWidget(Qt.RightDockWidgetArea, self.dw4) btn = QAction("A button",self) self.tb.addAction(btn) barTog = QAction(QIcon("arrange_24x24.png"),"Toggle Title Bars",self) barTog.setCheckable(True) barTog.triggered.connect(self.ToogleTitles2) self.tb.addAction(barTog) for widget in self.children(): if isinstance(widget, QTabBar): widget.setTabsClosable(True) widget.setCurrentIndex(0) break 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 getIconByType(self, Type): iconList = [ (1, "type1.png"), (3, "type2.png"), (2, "type3.png"), (4, "type4.png"), (5, "type5.png"), (6, "type6.png"), (7, "type7.png"), (8, "type8.png"), (9, "type9.png"), (10, "type10.png"), (11, "type11.png"), (12, "type12.png"), (13, "type13.png"), (14, "type14.png"), (15, "type15.png"), (16, "type16.png") ] for TypeID, Icon in iconList: if TypeID == Type: return Icon break 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 CreateNewTab(self): WNoStr = str(self.CountDockWidgets()) dw = QDockWidget(WNoStr) dw.topLevelChanged.connect(self.HideTitleBar) dw.setTitleBarWidget(QWidget(dw)) textArea = QTextEdit() textArea.setText("Text area " + WNoStr) dw.setWidget(textArea) dw.topLevelChanged.connect(self.HideTitleBar) self.addDockWidget(Qt.LeftDockWidgetArea, dw) self.tabifyDockWidget(self.dw1, dw) def HideTitleBar(self): dockw = self.sender() if dockw.isFloating() == False and self.ShowTitleBars == False: dockw.setTitleBarWidget(QWidget(dockw)) def ToogleTitles2(self): if self.ShowTitleBars == True: self.ShowTitleBars = False else: self.ShowTitleBars = True for widget in self.children(): if isinstance(widget, QDockWidget): if widget.titleBarWidget() == None and self.ShowTitleBars == False: if widget.isFloating() == False: widget.setTitleBarWidget(QWidget(widget)) else: widget.setTitleBarWidget(None) print("Test Toggle") def SelectionEvent(self, selIdxs): print("Selection changed!!!!!!!!!!!!!!!") @pyqtSlot("QModelIndex", "QModelIndex", "QVector<int>") def CurrentItemChanged(self, tep_left, bottom_right, roles): print("CurrentItemChanged") snd = self.sender() print("Sender = " + str(snd) + " " + str(snd.objectName())) print("Sender Data = " + str(snd.data)) print("Sender Data = " + str(tep_left.data)) getRowByIndex(top_left) # <<< based on index, do stuff #print("Selected iems = " + str(selItems)) def DroppedItem(self): print("Item dropped...") # ---------------------------------------------------------------------------------------------------------------------------------------------------------------- if __name__ == '__main__': import sys app = PyQt5.QtWidgets.QApplication(sys.argv) window = Window() window.show() app.exec_()
I don't think I can overcome it on my own, having basically no working example of what I'm trying to achieve, so I would like to ask the community here for advice.
-
-
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'".