Monday 12 April 2010

PyQt4 model/view drag & drop example

In PyQt4, drag and drop with QListWidget works really, really well.

However, for real life application, I believe it's best to leave the convenience widgets (like QListWidget) behind, and head for the model/view pyqt4 classes. This gives a lot more flexibility than the "one-size fits all" model which comes with QListwidget et al.

This enables easy referencing of the true python objects that underlie the model, using Qt.UserRole to refer to the object, and pickle (or cPickle) to convert to a bytestream which the drag/drop can handle.

here is a little example using a few tricks to get drag and drop of native python objects in PyQt4.




import datetime
import cPickle
import pickle
import sys

from PyQt4 import QtGui, QtCore

class person(object):
    '''
    a custom data structure, for example purposes
    '''
    def __init__(self, name, dob, house_no):
        self.name = name
        self.dob = dob
        self.addr = "%d Rue de la Soleil"% house_no
    def __repr__(self):
        return "%s\n%s\n%s"% (self.name, self.dob, self.addr)

class simple_model(QtCore.QAbstractListModel):
    def __init__(self, parent=None):
        super(simple_model, self).__init__(parent)
        self.list = []
        for name, dob, house_no in (
        ("Neil", datetime.date(1969,12,9), 23),
        ("John", datetime.date(1952,5,3), 2543),
        ("Ilona", datetime.date(1975,4,6), 1)):
            self.list.append(person(name, dob, house_no))
        self.setSupportedDragActions(QtCore.Qt.MoveAction)

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.list)

    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole: #show just the name
            person = self.list[index.row()]
            return QtCore.QVariant(person.name)
        elif role == QtCore.Qt.UserRole:  #return the whole python object
            person = self.list[index.row()]
            return person
        return QtCore.QVariant()

    def removeRow(self, position):
        self.list = self.list[:position] + self.list[position+1:]
        self.reset()

class dropZone(QtGui.QLabel):
    def __init__(self, parent=None):
        super(dropZone, self).__init__(parent)
        self.setMinimumSize(200,200)
        self.set_bg()
        self.setText("Drop Here")
        self.setAlignment(QtCore.Qt.AlignCenter)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat("application/x-person"):
            self.set_bg(True)
            event.accept()
        else:
            event.ignore()

    def dragMoveEvent(self, event):
        if event.mimeData().hasFormat("application/x-person"):
            event.setDropAction(QtCore.Qt.MoveAction)
            event.accept()
        else:
            event.ignore()

    def dragLeaveEvent(self, event):
        self.set_bg()

    def dropEvent(self, event):
        data = event.mimeData()
        bstream = data.retrieveData("application/x-person",
            QtCore.QVariant.ByteArray)
        selected = pickle.loads(bstream.toByteArray())
        self.setText(str(selected))
        self.set_bg()
        event.accept()

    def set_bg(self, active=False):
        if active:
            val = "background:yellow;"
        else:
            val = "background:green;"
        self.setStyleSheet(val)


class draggableList(QtGui.QListView):
    '''
    a listView whose items can be moved
    '''
    def ___init__(self, parent=None):
        super(draggableList, self).__init__(parent)
        self.setDragEnabled(True)

    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat("application/x-person"):
            event.setDropAction(QtCore.Qt.QMoveAction)
            event.accept()
        else:
            event.ignore()

    def startDrag(self, event):
        index = self.indexAt(event.pos())
        if not index.isValid():
            return

        ## selected is the relevant person object
        selected = self.model().data(index,QtCore.Qt.UserRole)

        ## convert to  a bytestream
        bstream = cPickle.dumps(selected)
        mimeData = QtCore.QMimeData()
        mimeData.setData("application/x-person", bstream)

        drag = QtGui.QDrag(self)
        drag.setMimeData(mimeData)

        # example 1 - the object itself

        pixmap = QtGui.QPixmap()
        pixmap = pixmap.grabWidget(self, self.rectForIndex(index))

        # example 2 -  a plain pixmap
        #pixmap = QtGui.QPixmap(100, self.height()/2)
        #pixmap.fill(QtGui.QColor("orange"))
        drag.setPixmap(pixmap)

        drag.setHotSpot(QtCore.QPoint(pixmap.width()/2, pixmap.height()/2))
        drag.setPixmap(pixmap)
        result = drag.start(QtCore.Qt.MoveAction)
        if result: # == QtCore.Qt.MoveAction:
            self.model().removeRow(index.row())

    def mouseMoveEvent(self, event):
        self.startDrag(event)

class testDialog(QtGui.QDialog):
    def __init__(self, parent=None):
        super(testDialog, self).__init__(parent)
        self.setWindowTitle("Drag Drop Test")
        layout = QtGui.QGridLayout(self)

        label = QtGui.QLabel("Drag Name From This List")

        self.model = simple_model()
        self.listView = draggableList()
        self.listView.setModel(self.model)
        self.dz = dropZone()

        layout.addWidget(label,0,0)
        layout.addWidget(self.listView,1,0)
        layout.addWidget(self.dz,0,1,2,2)

if __name__ == "__main__":
    '''
    the try catch here is to ensure that the app exits cleanly no matter what
    makes life better for SPE
    '''
    try:
        app = QtGui.QApplication([])
        dl = testDialog()
        dl.exec_()
    except Exception, e:  #could use as e for python 2.6...
        print e
    sys.exit(app.closeAllWindows())

2 comments:

Umeshnrao said...

Nice example :) Thanks. Will help me in using for my app.

Display Name said...

There is an extra _ before draggableList.__init__