9

Hey I had been going through this tutorial for understanding drag and drop methods in PyQt4. However I am not able to understand the following points . It would be nice if somepne could make it clearer to me.

 def mouseMoveEvent(self, e): //class Button


    mimeData = QtCore.QMimeData()

    drag = QtGui.QDrag(self)
    drag.setMimeData(mimeData)
    drag.setHotSpot(e.pos() - self.rect().topLeft())

    dropAction = drag.start(QtCore.Qt.MoveAction)

def dropEvent(self, e): //class Example

    position = e.pos()
    self.button.move(position)

    e.setDropAction(QtCore.Qt.MoveAction)
    e.accept()

Why is there are a seperate self.button.move() and e.setDropAction() Doesnt self.button.move() actually move the button itself? And could someone explain what drag.setHotSpot and drag.start() do? Thanks.

Manoj
  • 961
  • 4
  • 11
  • 37

2 Answers2

16

That tutorial is seriously outdated. QDrag.start is obsolete since Qt 4.3. QDrag.exec_ should be used instead.

As you can see from the docs for exec, it has a return value. setDropAction in dropEvent determines this value. It doesn't perform the move. That's why you need a self.button.move() to do the actual moving. So, what's the point of a setDropAction? You might need to know what kind of drag operation you did. Imagine you're implementing drag-drop between two list widgets. If you did a move operation, that means you need to remove the item from the source widget and create one in the target. If it was a copy operation, you can leave the original and just create a copy in the target.

setHotSpot/hotSpot is related to the setPixmap of a QDrag. You can display a QPixmap as you drag the item. hotSpot determines the positioning of the pixmap. The pixmap will be positioned such that the cursor will be at hotSpot relative to the top-left corner of the pixmap. So, in the case of that tutorial, it is rather pointless since there is no pixmap to be shown.

Here is a bit modified and updated version of that tutorial. Hopefully, I've included enough comments. You can move with Right-Click or copy with Shift + Right-Click:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
from PyQt4 import QtGui, QtCore


class Button(QtGui.QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() != QtCore.Qt.RightButton:
            return

        # write the relative cursor position to mime data
        mimeData = QtCore.QMimeData()
        # simple string with 'x,y'
        mimeData.setText('%d,%d' % (e.x(), e.y()))

        # let's make it fancy. we'll show a "ghost" of the button as we drag
        # grab the button to a pixmap
        pixmap = QtGui.QPixmap.grabWidget(self)

        # below makes the pixmap half transparent
        painter = QtGui.QPainter(pixmap)
        painter.setCompositionMode(painter.CompositionMode_DestinationIn)
        painter.fillRect(pixmap.rect(), QtGui.QColor(0, 0, 0, 127))
        painter.end()

        # make a QDrag
        drag = QtGui.QDrag(self)
        # put our MimeData
        drag.setMimeData(mimeData)
        # set its Pixmap
        drag.setPixmap(pixmap)
        # shift the Pixmap so that it coincides with the cursor position
        drag.setHotSpot(e.pos())

        # start the drag operation
        # exec_ will return the accepted action from dropEvent
        if drag.exec_(QtCore.Qt.CopyAction | QtCore.Qt.MoveAction) == QtCore.Qt.MoveAction:
            print 'moved'
        else:
            print 'copied'


    def mousePressEvent(self, e):
        QtGui.QPushButton.mousePressEvent(self, e)
        if e.button() == QtCore.Qt.LeftButton:
            print 'press'



class Example(QtGui.QWidget):
    def __init__(self):
        super(Example, self).__init__()
        self.initUI()


    def initUI(self):
        self.setAcceptDrops(True)

        button = Button('Button', self)
        button.move(100, 65)

        self.buttons = [button]

        self.setWindowTitle('Copy or Move')
        self.setGeometry(300, 300, 280, 150)


    def dragEnterEvent(self, e):
        e.accept()


    def dropEvent(self, e):
        # get the relative position from the mime data
        mime = e.mimeData().text()
        x, y = map(int, mime.split(','))

        if e.keyboardModifiers() & QtCore.Qt.ShiftModifier:
            # copy
            # so create a new button
            button = Button('Button', self)
            # move it to the position adjusted with the cursor position at drag
            button.move(e.pos()-QtCore.QPoint(x, y))
            # show it
            button.show()
            # store it
            self.buttons.append(button)
            # set the drop action as Copy
            e.setDropAction(QtCore.Qt.CopyAction)
        else:
            # move
            # so move the dragged button (i.e. event.source())
            e.source().move(e.pos()-QtCore.QPoint(x, y))
            # set the drop action as Move
            e.setDropAction(QtCore.Qt.MoveAction)
        # tell the QDrag we accepted it
        e.accept()



if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    ex = Example()
    ex.show()
    app.exec_()  
Avaris
  • 35,883
  • 7
  • 81
  • 72
  • Thanks for the wonderful answer. However I still have a doubt , Why do I move the button to e.pos() - QtCore.QPoint(x , y) . Doesn't e.pos() itself , give the position to where it should be dropped? . And doesnt mimeData give the position of the cursor becaues you have set it to e.x() and e.y() where e is the position of the mouse, and when I drop it , won't both e.pos() and QtCore.QPoint(x , y) . Sorry for my noobness. – Manoj Jan 19 '13 at 09:47
  • 1
    @Manoj: `x` and `y` coming from the `mime` are _local position_ of the cursor with respect to the button. `e.pos()` will be the actual position of the cursor on the widget (`Example`). If you were to move to button to `e.pos()`, it will move the top-left of the button to that position. For example, you picked the button at the middle of the button. And if you move to new cursor position with just `e.pos()`, button will be placed not the way you picked it up, but shifted. `x,y` correct that shift by adjusting the top-left of the button such that cursor will be at the same position after drop. – Avaris Jan 19 '13 at 10:27
  • 1
    @Manoj: I guess your confusion comes from `e.x()/e.y()` vs `e.pos()` in the code. The part where I get `e.x()/e.y()` is the `mouseMoveEvent` of the `Button`. So they are relative to the button (`0,0` being the top-left). Where as `e.pos()` in the `dropEvent` of `Example`. The position it will give will be the relative position of the cursor with respect to `Example`. – Avaris Jan 19 '13 at 10:34
  • Yes Ive understood now , Thanks for your time. – Manoj Jan 19 '13 at 10:39
  • It would be really nice , if you could help me with this piece of code also. http://stackoverflow.com/questions/14606192/rotate-transformoriginpoint-pyqt4 . Sorry for spamming. – Manoj Jan 31 '13 at 05:56
  • I was going to ask a question on what a hotspot was, as I couldn't find what I was looking for in the api references I was looking at. You explained it quite clearly, thank you. – NuclearPeon Feb 28 '14 at 12:24
  • @Avaris if i'm under linux , and setCompositionMode is not supported, how would i use a semi-transparent QImage instead of a QPixmap while dragging ? thanks ! – Shuman Jul 22 '14 at 00:56
  • @Shuman: What do you mean by 'setCompositionMode is not supported'? The code above works perfectly fine on my Ubuntu 14.04. – Avaris Jul 22 '14 at 03:24
  • @Avaris on some system it's not working, at least for my centos 6.5, i can't update it because it's on a office machine. see [here](http://qt-project.org/doc/qt-4.8/qpainter.html#setCompositionMode) – Shuman Jul 22 '14 at 03:48
  • @Shuman: Ok, you can convert `QPixmap` to `QImage` (`image = pixmap.toImage()`), apply composition and convert it back (`new_pixmap = QPixmap.fromImage(image)`) – Avaris Jul 22 '14 at 04:07
1

Avaris' answer adapted for PyQt5 and Python 3.

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Adapted for PyQt5 and Python 3 from Avaris' answer to
# https://stackoverflow.com/questions/14395799/pyqt4-drag-and-drop

import sys
from PyQt5.QtWidgets import QPushButton, QWidget, QApplication
from PyQt5.QtCore import Qt, QMimeData, QPoint
from PyQt5.QtGui import QDrag, QPixmap, QPainter, QColor


class Button(QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() != Qt.RightButton:
            return

        # write the relative cursor position to mime data
        mimeData = QMimeData()
        # simple string with 'x,y'
        mimeData.setText('%d,%d' % (e.x(), e.y()))

        # let's make it fancy. we'll show a "ghost" of the button as we drag
        # grab the button to a pixmap
        pixmap = QWidget.grab(self)

        # below makes the pixmap half transparent
        painter = QPainter(pixmap)
        painter.setCompositionMode(painter.CompositionMode_DestinationIn)
        painter.fillRect(pixmap.rect(), QColor(0, 0, 0, 127))
        painter.end()

        # make a QDrag
        drag = QDrag(self)
        # put our MimeData
        drag.setMimeData(mimeData)
        # set its Pixmap
        drag.setPixmap(pixmap)
        # shift the Pixmap so that it coincides with the cursor position
        drag.setHotSpot(e.pos())

        # start the drag operation
        # exec_ will return the accepted action from dropEvent
        if drag.exec_(Qt.CopyAction | Qt.MoveAction) == Qt.MoveAction:
            print('moved')
        else:
            print('copied')


    def mousePressEvent(self, e):
        QPushButton.mousePressEvent(self, e)
        if e.button() == Qt.LeftButton:
            print('press')



class Example(QWidget):
    def __init__(self):
        super(Example, self).__init__()
        self.initUI()


    def initUI(self):
        self.setAcceptDrops(True)

        button = Button('Button', self)
        button.move(100, 65)

        self.buttons = [button]

        self.setWindowTitle('Copy or Move')
        self.setGeometry(300, 300, 280, 150)


    def dragEnterEvent(self, e):
        e.accept()


    def dropEvent(self, e):
        # get the relative position from the mime data
        mime = e.mimeData().text()
        x, y = map(int, mime.split(','))

        if e.keyboardModifiers() & Qt.ShiftModifier:
            # copy
            # so create a new button
            button = Button('Button', self)
            # move it to the position adjusted with the cursor position at drag
            button.move(e.pos()-QPoint(x, y))
            # show it
            button.show()
            # store it
            self.buttons.append(button)
            # set the drop action as Copy
            e.setDropAction(Qt.CopyAction)
        else:
            # move
            # so move the dragged button (i.e. event.source())
            e.source().move(e.pos()-QPoint(x, y))
            # set the drop action as Move
            e.setDropAction(Qt.MoveAction)
        # tell the QDrag we accepted it
        e.accept()



if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    ex.show()
    app.exec_()
David Wallace
  • 212
  • 4
  • 12