[PyQt] Moving around objects with a Tree Model View

hey guys, I am trying to create a Tree Model View with support for re-ordering the objects within the tree view. I’m stumbling on some of the concepts and requirements to get this working.

Most of my code is based on Yasin’s excellent model view programming tutorials.

I am at a point where I can see the tree view, items are draggable and will accept drops, but I am encountering some errors and unwanted behaviors.

When I try to drop an object, I am getting a “list index out of range” error, triggered by the removeRows function.
The original object is getting deleted and a new object is getting created instead of the original object getting moved. How to I get to the object being moved so I can re-parent it in the Model?

Can someone explain how drag and drop works in pyqt?

Thanks.

Hey!

To get drag and drop support for the model/view framework you have to override 3 methods inside your model.

First one is the mimeTypes function. Obviously you would use a custom mime type identifier or one that is already available and widely used by many applications, Qt has support functions for them as well, check the documentation for mimetypes.


    def mimeTypes(self):
        return [ "application/x-tech.artists.org" ]

Second method would be mimeData which receives a list of QModelIndex for objects you have selected and started a drag operation. Your job in this method is to pack them into a QMimeData object using a QDataStream and return it, again check the documentation when you want to use common mimetypes such as text and images.


    def mimeData(self, indices):

        mimeData      = QtCore.QMimeData()
        encodedData = QtCore.QByteArray()
        stream         = QtCore.QDataStream(encodedData, QtCore.QIODevice.WriteOnly)

        for index in indices:
            if not index.isValid():
                continue
            node = index.internalPointer()
                
            variant = QtCore.QVariant(node)
                
            # add all the items into the stream
            stream << variant
                
        print "Encoding drag with: ", "application/x-tech.artists.org"
        mimeData.setData("application/x-tech.artists.org", encodedData)
        return mimeData

The final one would be dropMimeData which receives the QMimeData in the data parameter, action is the type of drag (Copy or Move), row and column is where within the parent (if it exists) you want to drop it. If you don’t use multi row multi column tree view you can just ignore the column parameter.

This last method is not final and might contain errors since I wrote it on top of my head but it should get you started with dropping for the model/view framework.
Read the comments it should be useful guidance.

This method


    def dropMimeData(self, data, action, row, column, parent):

        if action == QtCore.Qt.CopyAction:
            print "Copying"
        elif action == QtCore.Qt.MoveAction:
            print "Moving"
        print "Param data:", data
        print "Param row:",  row
        print "Param column:", column
        print "Param parent:", parent

        # Where are we inserting?
        beginRow = 0
        if row != -1:
            print "ROW IS NOT -1, meaning inserting inbetween, above or below an existing node"
            beginRow = row
        elif parent.isValid():
            print "PARENT IS VALID, inserting ONTO something since row was not -1, beginRow becomes 0 because we want to insert it at the begining of this parents children"
            beginRow = 0
        else:
            print "PARENT IS INVALID, inserting to root, can change to 0 if you want it to appear at the top"
            beginRow = self.rowCount(QtCore.QModelIndex())

        # create a read only stream to read back packed data from our QMimeData
        encodedData = data.data("application/x-tech.artists.org")

        stream = QtCore.QDataStream(encodedData, QtCore.QIODevice.ReadOnly)


        # decode all our data back into dropList
        dropList = []
        numDrop = 0

        while not stream.atEnd():
            variant = QtCore.QVariant()
            stream >> variant # extract
            node = variant.toPyObject()
            
            # add the python object that was wrapped up by a QVariant back in our mimeData method
            dropList.append( node ) 

            # number of items to insert later
            numDrop += 1


        print "INSERTING AT", beginRow, "WITH", numDrop, "AMOUNT OF ITEMS ON PARENT:", parent.internalPointer()
        
        # This will insert new items, so you have to either update the values after the insertion or write your own method to receive our decoded dropList objects.
        self.insertRows(beginRow, numDrop, parent) 
        
        for drop in dropList:
            # If you don't have your own insertion method and stick with the insertRows above, this is where you would update the values using our dropList.
            pass

Depending on the flags you give to your QTreeView (such as InternalMove), it will automatically remove the “dragged” object from itself if its a move action so you don’t have to do that in the model, which is the reason why we don’t do anything different when doing a move vs copy. Besides the data could come from a completely different view with different model >_^

Hope this helps.

Goodluck!

Thanks for the help! I will give this a go. I decided to go back to a regular old QTreeWidget since it handles drag and drop already and for this utility a full-blown model-view isn’t really necessary (I was just trying to stretch my legs!).

Since the items in my tree view are just regular widgets with a name , what kind of mime type should I use and why does it matter? The mime stuff really has me confused…

thanks again for the in-depth explanation!

I copied some text from this documentation: http://qt-project.org/doc/qt-4.8/qmimedata.html#details

[I]The QMimeData class provides a container for data that records information about its MIME type.
QMimeData is used to describe information that can be stored in the clipboard, and transferred via the drag and drop mechanism. QMimeData objects associate the data that they hold with the corresponding MIME types to ensure that information can be safely transferred between applications, and copied around within the same application.

For the most common MIME types, QMimeData provides convenience functions to access the data:[/I]


Tester	        Getter	      Setter	         MIME Types
hasText()	text()	      setText()	         text/plain
hasHtml()	html()	      setHtml()	         text/html
hasUrls()	urls()	      setUrls()          text/uri-list
hasImage()	imageData()   setImageData()	 image/ *
hasColor()	colorData()   setColorData()	 application/x-color

So if you use setImageData or setColorData…


mimeData.setImageData( QtGui.QImage("beautifulfjord.png") )
or
mimeData.setColorData( QtGui.QColor(168, 34, 3) )

…to encode your object during drag/drop operation then you could go and drop that right into other applications accepting images or colors from the clipboard. But if you have custom objects such as “MyCustomNode” you would have to use a custom mimetype. If you have several applicaitons then understanding this custom mime type they could start accepting drag and drops for that type, again QDataStream and QVariant makes this very easy since it’s basically serializing our object.

thanks for the explanation, I guess since my utility is so simple, the mime data seems extra complex. I don’t need to share my TreeWidgets with other applications or windows or models. I just want to re-order the tree and then make a change in maya.

(i’m trying to make a very simple set editor to replace the clunky native maya set editor)

Alright :slight_smile: I assumed you have custom objects like “MyCustomNode” when you said you use the model/view framework. If you are using QTreeWidget and QTreeWidgetItem you can ignore QMimeData all together since it handles the moving around and copying, but it tightly couples your data object to the GUI representation.

Thanks for the help, I started off with the Model/View Stuff, but I switch to a simple TreeWidget. I’ll go back and dig around some more - thanks!

This thread has been very usefull for my own problem, although I have a question, since my situation is a bit different… how would you save the data to the stream if the object you are trying to save contains the weakref?
I am using QTreeView and QAbstractItemModel but in PyQt and my objects contain the weakref… so every time I am trying to pass the data to the QDataStream, it is complaining with error msg:
# TypeError: can’t pickle weakref objects
Any idea for this one?

Hey @LoneWolf I’ve been following your YouTube videos on model/view etc (awesome by the way) and have hit a wall on drag and dropping tree view items using a custom Node class (just like the one you’re describing in your videos)… I’m receiving an error where you are assigning childItem = parentNode.child(row) in the tree model’s “index” method when I drop a tree view item onto another tree view item.

I would be super grateful if you would like to have a look… I created a stackoverflow question with the entire code:
https://stackoverflow.com/questions/50374042/how-to-drag-and-drop-items-from-multiple-views-into-qtreeview

Cheers,
Fredrik