Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

QTreeView drag/drop handling



  • I have some obvious questions for which I wasn't able to find satisfactory answers.

    1. 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 same ID and Name. That is obviously unacceptable, ID should be a new one (typically obtained from a database) and Name should be somtehing like "OriginalName_1".

    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).

    1. Is there a way to set QTreeView in a way, that drag&drops within a QTreeView would move items (they're copied by default), while dropping from another QTreeView 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 all treeModel 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.


  • Lifetime Qt Champion

    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 or QStandardItemModel, 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() of isValid() == 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.



  • @Oak77
    My suggestion: if it were me looking to figure this out or ask for help, I would reduce the size of your program and its vast number of imports down to something minimal for myself/others to look at.



  • @JonB Well, it's dramatically reduced already to what I thought was close to minimum, however I'll try to review it again. I do see few bits I can get rid off. But obviously, I need to keep two tree views and some basic stuff to make it work.



  • 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 which QTreeViews 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.


  • Lifetime Qt Champion

    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 reimplemented mimeData...(?). 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 the index, out of indexed row the data(), which should be an ID and get a datarow by that ID. Something I would do in VB.net using a ValueMember 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) of index.internalPointer(). I took the original code from some examples, but they were used in QAbsstractModel, while I'm using QStandardItemModel. 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!


  • Lifetime Qt Champion

    '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 a QByteArray/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.)


  • Lifetime Qt Champion

    The encode method returns a bytes object.



  • @JonB and @SGaist

    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 to data, 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 :-)


  • Lifetime Qt Champion

    @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'".



  • @SGaist Lost am I :-). I see you pointing out python bytes vs. QByteArray, makes sense, I'll study that. Thank you for caring to comment my rookie issues.


Log in to reply