QT Drag and Drop help

1,024 views
Skip to first unread message

Benjam901

unread,
Apr 22, 2015, 9:46:21 AM4/22/15
to python_in...@googlegroups.com
Hello all,

I am having a bit of trouble with my QT Drag and Drop events. I am dragging an item from a QTableWidget (uiNodeUnit) and dropping it onto a QTreeWidget (uiUnitTree)

I have managed to set up an event filter on the QTreeWidget and when the item is dropped the event is fired. However I am struggling to return the name of the item that has been dragged in, I have looked into startDrag and mime data but no luck on my end so far. Is there some method that I am missing.

One extra thing, when I print source.objectName() the value is very odd indeed. The value I get returned is: qt_scrollarea_viewport should it be one of my widgets or have I set it up incorrectly?

Cheers,

Ben

My code is as follows:

12345678910111213
# Inside the __init__ function
self.uiMainWindow.uiNodeUnit.setDragEnabled(True)
 
self.uiMainWindow.uiUnitTree.setDropIndicatorShown(True)
self.uiMainWindow.uiUnitTree.setAcceptDrops(True)
self.uiMainWindow.uiUnitTree.setDragEnabled(True)
self.uiMainWindow.uiUnitTree.viewport().installEventFilter(self)
 
def eventFilter(self, source, event):
#print source.objectName()
if (event.type() == QtCore.QEvent.Drop):
print "Event Filter"
return QtGui.QMainWindow.eventFilter(self, source, event)

Joe Weidenbach

unread,
Apr 22, 2015, 11:45:08 AM4/22/15
to python_in...@googlegroups.com
Hi Ben,

There's a lot of ways to approach Drag and Drop, and none of them are particularly straightforward in my experience.  The key to remember is that what is passed in a drag and drop operation is Mime Data (as you indicated you looked into) and not a direct object.  My initial guess (and this is just that, so take it with a grain of salt) is that your event filter is responding to the source of the drop event (and qt_scrollarea_viewport is the internal viewport of the QTreeView where you installed the filter), not the source of the drag.  Generally what I've had the most success with in drag and drop was encoding a set of data into a custom mime-type, and then in your drop handler you can read that mime data.  Everything else has been unreliable.  I'll whip up some sample code for you today and send it over.

In the meantime, the specific method(s) you're looking for (at least what I've had the most success with) is dropMimeData() in your TreeWidget (the destination), and mimeData() in your TableWidget (the source).  mimeData(items) will encode data as you choose into a (potentially custom) mimeType, and then dropMimeData(parent, index, data, action) will decode that data and handle the results of the drop.

With that said, I've primarily worked in the QAbstractItemModel, QTreeView, and QTableView version so I could separate the model out, so this might be higher complexity than you're looking for.  I'll let you know.
--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/d689de76-6699-41db-a808-8e3eca1fce5e%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.




This email is free from viruses and malware because avast! Antivirus protection is active.


Benjam901

unread,
Apr 22, 2015, 12:08:37 PM4/22/15
to python_in...@googlegroups.com
Hello Joe,

That would be a massive help thank you very much.

I have found something online that has mentioned decoding the mimedata using QDataStream which indicates to me that the data is binary. If this is the case I would really like to be able to set my mime data myself so that decoding it is a little easier. That being said, my current experience in encoding/decoding binary data is limited. Would this be a good path to wander down?


Cheers,

Ben

Joe Weidenbach

unread,
Apr 22, 2015, 12:40:13 PM4/22/15
to python_in...@googlegroups.com
Well, speaking from the perspective of custom QAbstractItemModel with QTreeViews or QTableViews, that's EXACTLY the path.  I'm going to test with the Convenience widgets though, so I'll keep you posted.  The basics are these: mime data is binary, and you can attach it to an arbitrary label of datatype.  That data type can be whatever you want it to be (although there are some standard ones such as 'text/html' and whatnot.  I personally make my own, eg 'application/x-hierarchy-block'.  Then you just push a stream of the data you want to encode.  In several of my projects, that was the output of the repr() method for the object being dragged.  in others, it might be a node id, or pretty much anything you want.  It just depends on the use case.

I keep trying to find a use case for the convenience widgets (QTreeWidget etc), and at least in my experience, while they're great for one-time use data, most of my projects use persistance, so I've ended up going more into the custom models and hooking up views.  When you go that route, though, there's a lot more boilerplate you have to set up, so keep that in mind.

As I said, I'll get something coded up today to test it out.

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.

Ben Hearn

unread,
Apr 22, 2015, 1:57:41 PM4/22/15
to python_in...@googlegroups.com
Ok awesome thanks for the help. I was digging at it earlier and when I printed my mimedata using 

For format in mimedata.mimeFormats():
Print mimedata(format)

(From my iPhone so something along those lines) 

The name of the tablewidget I dragged from was present but it was surrounded by empty spaces. Is this just a matter of decoding it properly?
You received this message because you are subscribed to a topic in the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/python_inside_maya/_6fvzObx1Ds/unsubscribe.
To unsubscribe from this group and all its topics, send an email to python_inside_m...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/CAM33%3Da5rW8Dus%2BWgOSb888GHdtrHc7B9Y11WXAkkWbnENNS8RQ%40mail.gmail.com.

For more options, visit https://groups.google.com/d/optout.


--

Tel - +46 76245 92 90 (Sweden)

Joe Weidenbach

unread,
Apr 22, 2015, 3:34:19 PM4/22/15
to python_in...@googlegroups.com
For the simple solution, yes, it's a matter of decoding the mimeData.  If you want to do something custom though, you're going to need to encode the mimeData yourself in the source TableWidget (for example, if you want to transfer the whole row).  It's a good exercise regardless for when you get to using custom QAbstractItemModels.

I should have some sample code for you relatively shortly :)

Joe Weidenbach

unread,
Apr 22, 2015, 3:54:23 PM4/22/15
to python_in...@googlegroups.com
Ok, I got a working example of what I think you're looking for.  I probably went a little overboard on it, but I like to take control of various aspects of the drag and drop operations, and it's decent reference for if you want to do so.  It's pretty dirty at the moment, so if you have questions feel free to ask.  I basically extended the QTableWidget and QTreeWidget to get the behavior I was going for, and then used those in my main window.  You can drag from the table widget to the tree widget.  I encoded my mime data using json before passing it through the bitstream.

```python
import json

from PySide import QtCore, QtGui

class SourceWidget(QtGui.QTableWidget):
    def __init__(self, parent=None):
        super(SourceWidget, self).__init__(parent)
        self.setDragEnabled(True)

    def startDrag(self, supportedActions):
        items = self.selectedItems()
        indexes = self.selectedIndexes()
        if len(items) > 0:
            data = self.mimeData(items)
        if not data:
            return

        pixmap, rect = self.__renderToPixmap(indexes)
        drag = QtGui.QDrag(self)
        drag.setPixmap(pixmap)
        drag.setMimeData(data)
        drag.setHotSpot(self.__mouseClickPosition - rect.topLeft())
        drag.exec_(supportedActions, QtCore.Qt.CopyAction)

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            self.__mouseClickPosition = event.pos()

        super(SourceWidget, self).mousePressEvent(event)

    def mimeTypes(self):
        return ['application/x-my-custom-mimetype']

    def mimeData(self, items):
        if len(items) == 0:
            return False

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

        for item in items:
            firstColData = self.item(item.row(), 0).data(QtCore.Qt.DisplayRole)
            secondColData = self.item(item.row(), 1).data(QtCore.Qt.DisplayRole)
            output = (firstColData, secondColData)
            stream << json.dumps(output)

        mimeData.setData('application/x-my-custom-mimetype', data)
        return mimeData

    def __renderToPixmap(self, indexes):
        rect, paintPairs = self.__draggablePaintPairs(indexes)
        if len(paintPairs) == 0:
            return QtGui.QPixmap()
        pixmap = QtGui.QPixmap(rect.width(), rect.height())
        pixmap.fill(QtCore.Qt.transparent)

        painter = QtGui.QPainter(pixmap)
        option = self.viewOptions()
        option.state |= QtGui.QStyle.State_Selected
        for r, index in paintPairs:
            option.rect = r.translated(-r.topLeft())
            self.itemDelegate(index).paint(painter, option, index)
        return (pixmap, rect)

    def __draggablePaintPairs(self, indexes):
        rect = QtCore.QRect()
        viewportRect = self.viewport().rect()
        ret = []
        for index in indexes:
            current = self.visualRect(index)
            if current.intersects(viewportRect):
                ret.append((current, index))
                rect = rect | current
        rect = rect & viewportRect
        return (rect, ret)

class DestinationWidget(QtGui.QTreeWidget):
    def __init__(self, parent=None):
        super(DestinationWidget, self).__init__(parent)
        self.setAcceptDrops(True)
        self.setDropIndicatorShown(True)
        self.setDefaultDropAction(QtCore.Qt.CopyAction)
        self.setDragDropMode(QtGui.QAbstractItemView.DropOnly)

    def supportedDropActions(self):
        return QtCore.Qt.CopyAction

    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat('application/x-my-custom-mimetype'):
            event.acceptProposedAction()

    def dragMoveEvent(self, event):
        event.accept()

    def dropMimeData(self, parent, index, data, action):
        if not data.hasFormat('application/x-my-custom-mimetype'):
            return False

        encodedData = data.data('application/x-my-custom-mimetype')
        stream = QtCore.QDataStream(encodedData, QtCore.QIODevice.ReadOnly)
        jsonData = stream.readString()
        dropData = json.loads(jsonData)

        newItem = QtGui.QTreeWidgetItem(parent)
        newItem.setText(0, dropData[0])
        newItem.setText(1, dropData[1])
        parent.addChild(newItem)

        return True

class MainWindow(QtGui.QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        layout = QtGui.QHBoxLayout()

        source = SourceWidget(self)
        source.setRowCount(5)
        source.setColumnCount(2)
        for i in range(5):
            source.setItem(i, 0, QtGui.QTableWidgetItem("Item {0}".format(i+1)))
            source.setItem(i, 1, QtGui.QTableWidgetItem("Item {0} Col 2".format(i+1)))
        layout.addWidget(source)

        dest = DestinationWidget(self)
        dest.setColumnCount(2)
        top = QtGui.QTreeWidgetItem(dest)
        top.setText(0, "Root")
        dest.addTopLevelItem(top)
        layout.addWidget(dest)

        self.setLayout(layout)

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)

    win = MainWindow()
    win.show()

    sys.exit(app.exec_())
```

Joe Weidenbach

unread,
Apr 22, 2015, 3:54:52 PM4/22/15
to python_in...@googlegroups.com
and apparently my markdown here is not working yet :)

Ben Hearn

unread,
Apr 22, 2015, 4:10:07 PM4/22/15
to python_in...@googlegroups.com
Thank you very much that is a great example. I will take a solid dig into it ASAP. Just a point of reference I should have mentioned earlier. I have created my GUI using QT designer would this affect any of the behaviour? i.e. If I am setting dragEnabled and such in the __init__ function of my main window would overriding the mouse event and startdrag functions etc. still apply individually if I set them correctly or would I have to create some sort of workaround in this case?
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/CAM33%3Da5t_GdfYDfv%3DCfQUE7M3P1089HQ0hPy7FXA_p-VYp6Wew%40mail.gmail.com.

For more options, visit https://groups.google.com/d/optout.

Joe Weidenbach

unread,
Apr 22, 2015, 4:20:47 PM4/22/15
to python_in...@googlegroups.com
I don't honestly know if you can just override what's there, I can experiment a little bit though.  I know you *CAN* create a plugin for Qt Designer with your custom class to use, but it might be easier for that purpose to put in a layout or frame where your Child Widgets will go, and then set them up in code, parented to the frame you made.  Not clean, but it could work to shoehorn it in so you don't lose work.  It might make things a bit more complex in the long run.  That's the tricky part of designer--it's great when you just want to use base widgets, but it's a lot of overhead to build a plugin for every custom widget you want to reuse.  And I override so many things nowadays, I find I'm very rarely using base widgets in my main window layout.  If I do use Designer (It does make adjusting layout easier for complex projects), it's to make a custom widget, which I can then build my code for, and then I can include it in my main window from code.

Joe Weidenbach

unread,
Apr 22, 2015, 4:30:53 PM4/22/15
to python_in...@googlegroups.com
Ok, quick answer :)

It can work to assign the functions in your code for your design window (at least it works for how I'm doing it, haven't tried from the actual design window).  I only did this for the TreeWidget, but it should work for the TableWidget too.  I definitely don't like this code as much, but it DOES work.

```python
class MainWindow(QtGui.QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        layout = QtGui.QHBoxLayout()

        source = SourceWidget(self)
        source.setRowCount(5)
        source.setColumnCount(2)
        for i in range(5):
            source.setItem(i, 0, QtGui.QTableWidgetItem("Item {0}".format(i+1)))
            source.setItem(i, 1, QtGui.QTableWidgetItem("Item {0} Col 2".format(i+1)))
        layout.addWidget(source)

        dest = QtGui.QTreeWidget(self)
        dest.supportedDropActions = self.DestSupportedDropActions
        dest.dragEnterEvent = self.DestDragEnterEvent
        dest.dragMoveEvent = self.DestDragMoveEvent
        dest.dropMimeData = self.DestDropMimeData
        dest.setAcceptDrops(True)
        dest.setDropIndicatorShown(True)
        dest.setDefaultDropAction(QtCore.Qt.CopyAction)
        dest.setDragDropMode(QtGui.QAbstractItemView.DropOnly)
        dest.setColumnCount(2)
        top = QtGui.QTreeWidgetItem(dest)
        top.setText(0, "Root")
        dest.addTopLevelItem(top)
        layout.addWidget(dest)

        self.setLayout(layout)

    def DestSupportedDropActions(self):
        return QtCore.Qt.CopyAction

    def DestDragEnterEvent(self, event):
        if event.mimeData().hasFormat('application/x-my-custom-mimetype'):
            event.acceptProposedAction()

    def DestDragMoveEvent(self, event):
        event.accept()

    def DestDropMimeData(self, parent, index, data, action):
        if not data.hasFormat('application/x-my-custom-mimetype'):
            return False

        encodedData = data.data('application/x-my-custom-mimetype')
        stream = QtCore.QDataStream(encodedData, QtCore.QIODevice.ReadOnly)
        jsonData = stream.readString()
        dropData = json.loads(jsonData)

        newItem = QtGui.QTreeWidgetItem(parent)
        newItem.setText(0, dropData[0])
        newItem.setText(1, dropData[1])
        parent.addChild(newItem)

        return True

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)

    win = MainWindow()
    win.show()

    sys.exit(app.exec_())
```

Joe Weidenbach

unread,
Apr 22, 2015, 4:40:48 PM4/22/15
to python_in...@googlegroups.com
You might also look into this process using Designer to "promote" your TreeWidget and TableWidget in the designer:


Haven't tried it, might be worthwhile though.

Justin Israel

unread,
Apr 22, 2015, 4:55:49 PM4/22/15
to python_in...@googlegroups.com
Back when I used to use Designer, I also preferred the approach of just "blocking out" a spot in the layout where I would then add a custom widget in code. That way you can still have a clean custom class implementation of your own. 

Joe Weidenbach

unread,
Apr 22, 2015, 5:21:33 PM4/22/15
to python_in...@googlegroups.com
Pretty much my feeling too, but if you've got to get up and running quick, this could work :)

Benjam901

unread,
Apr 23, 2015, 6:51:01 AM4/23/15
to python_in...@googlegroups.com
Hello Joe,

Thanks for the great amount of help. I have learnt a lot from digging through your code and will use it as an exercise to get my exp up with custom models and such in the future. As for decoding the mime data goes, I managed to retrieve it using a great source on StackOverflow (http://stackoverflow.com/questions/8609794/pyqt-mimedata-to-string-conversion-how-do-i-eliminate-some-spaces-but-not-all) and it now returns a string of the item dragged in. 

Any normal person would be happy with that but since I don't fully understand what is going on I personally feel this is useless as I have not learnt anything and if a problem should arise I would not be able to debug it myself.

Would you be able to help me out in understanding what is going on. Does Qt generate on drag operations default mimeData from events, Why do we have to pass them into streams and then decode them etc. 

Here is what I have:

# Drag and drop setup inside the __init__ class using QtDesigner windows
self.uiMainWindow.uiNodeUnit.setDragEnabled(True)
 
self.uiMainWindow.uiUnitTree.setDropIndicatorShown(True)
self.uiMainWindow.uiUnitTree.setAcceptDrops(True)
self.uiMainWindow.uiUnitTree.setDragEnabled(True)
self.uiMainWindow.uiUnitTree.viewport().installEventFilter(self)
 
# TODO: Decode mime data
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.Drop):
mimeData = event.mimeData()
for mimeFormat in mimeData.formats():
# we only want to attempt to decode QAbstractItem's
if mimeFormat != "application/x-qabstractitemmodeldatalist":
continue
data = self.decodeMimeData(mimeData.data(mimeFormat))
 
elif (event.type() == QtCore.QEvent.DragEnter):
print "DRAGGING"
return QtGui.QMainWindow.eventFilter(self, source, event)
 
def decodeMimeData(self, mimeData):
result = {}
value = QtCore.QVariant()
stream = QtCore.QDataStream(mimeData)
 
while not stream.atEnd():
# row and column for the data
row = stream.readInt32()
col = stream.readInt32()
item = result.setdefault(col, {})
for role in range(stream.readInt32()):
key = QtCore.Qt.ItemDataRole(stream.readInt32())
stream >> value
item[key] = value.toPyObject()
return result[0][QtCore.Qt.DisplayRole]
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

--
You received this message because you are subscribed to a topic in the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/python_inside_maya/_6fvzObx1Ds/unsubscribe.
To unsubscribe from this group and all its topics, send an email to python_inside_maya+unsub...@googlegroups.com.


--

Tel - +46 76245 92 90 (Sweden)

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

--
You received this message because you are subscribed to a topic in the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/python_inside_maya/_6fvzObx1Ds/unsubscribe.
To unsubscribe from this group and all its topics, send an email to python_inside_maya+unsub...@googlegroups.com.


--

Tel - +46 76245 92 90 (Sweden)

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

Justin Israel

unread,
Apr 23, 2015, 7:06:47 AM4/23/15
to python_in...@googlegroups.com

The QDataStream approach is very C++ centric. Also the particular mimetype you are handling in this example happens to be Qt's internal mimetype spec for communicating item drag and drops. That means the default drag and drop behavior does generate it's own mimetype protocol.

When you implement your own drag routine, either by implementing the high level mimeData or the lower level startDrag or the actual events themselves, you get to control how you want to talk between the drag source and the drop target. You can specify your own custom type, and send either plain text, or even json or pickle data. Qt happens to use its binary protocol to transmit serialized data.

Your code example happens to be decoding the builtin mimetype for internal drag and drops. It streams the data out of the mimetype in the expected order.


To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.

--
You received this message because you are subscribed to a topic in the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/python_inside_maya/_6fvzObx1Ds/unsubscribe.
To unsubscribe from this group and all its topics, send an email to python_inside_m...@googlegroups.com.


--

Tel - +46 76245 92 90 (Sweden)

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.

--
You received this message because you are subscribed to a topic in the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/python_inside_maya/_6fvzObx1Ds/unsubscribe.
To unsubscribe from this group and all its topics, send an email to python_inside_m...@googlegroups.com.


--

Tel - +46 76245 92 90 (Sweden)

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/3a0d36c9-f788-4234-9ec5-b6255a3750f2%40googlegroups.com.

Ben Hearn

unread,
Apr 23, 2015, 2:08:30 PM4/23/15
to python_in...@googlegroups.com
Hello Justin,

Thanks for the knowledge boost, I will be keeping that in mind. I may try to create my own custom mimeData later on once I have gotten the current system down if I feel I would benefit from it. 

Cheers,

Ben


For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages