9

I am really struggling to figure out a way to do this. Say I implement a button very simply in a widget window:

self.button = QPushButton("Drag Me", self)

I can move its initialization point around the parent widget's area using self.button.move(x,y), and I can get mouse events from mousePressEvent(self, e) via e.x() and e.y(), so that the button moves to wherever I click, but I just cannot seem to put all this together into a drag and drop framework.

Clarification: After reading on the 'true' meaning of Drag/Drop, that's not what I need. I just want to be able to move a widget around with my mouse, much similar to the way you move magnets on a fridge.

RodericDay
  • 1,266
  • 4
  • 20
  • 35
  • 1
    @Eric makes a very good point in his answer. Could you please clarify this question as to whether you want true drag and drop events...or just to simply be able to move the button around with the mouse – jdi Aug 31 '12 at 17:44
  • 1
    Based on what you're intending to do - I would look into the QGraphicsView framework. What you are trying to do (virtual magnet board) would be very easily accomplished by that. – Eric Hulser Aug 31 '12 at 22:17

2 Answers2

18

Here is an example of a moveable button that still supports the normal click signal properly:

from PyQt4 import QtCore, QtGui


class DragButton(QtGui.QPushButton):

    def mousePressEvent(self, event):
        self.__mousePressPos = None
        self.__mouseMovePos = None
        if event.button() == QtCore.Qt.LeftButton:
            self.__mousePressPos = event.globalPos()
            self.__mouseMovePos = event.globalPos()

        super(DragButton, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() == QtCore.Qt.LeftButton:
            # adjust offset from clicked point to origin of widget
            currPos = self.mapToGlobal(self.pos())
            globalPos = event.globalPos()
            diff = globalPos - self.__mouseMovePos
            newPos = self.mapFromGlobal(currPos + diff)
            self.move(newPos)

            self.__mouseMovePos = globalPos

        super(DragButton, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if self.__mousePressPos is not None:
            moved = event.globalPos() - self.__mousePressPos 
            if moved.manhattanLength() > 3:
                event.ignore()
                return

        super(DragButton, self).mouseReleaseEvent(event)

def clicked():
    print "click as normal!"

if __name__ == "__main__":
    app = QtGui.QApplication([])
    w = QtGui.QWidget()
    w.resize(800,600)

    button = DragButton("Drag", w)
    button.clicked.connect(clicked)

    w.show()
    app.exec_()

In the mousePressEvent I record both the initial start position, and a position that will get updated throughout the drag.

In the mouseMoveEvent, I get the proper offset of the widget from where it was clicked to where the actual origin is, so that the move is accurate.

In the mouseReleaseEvent, I check to see if the overall move was greater than at least a tiny amount. If it was, then it was a drag and we ignore the normal event to not produce a "clicked" signal. Otherwise, we allow the normal event handler to produce the click.

jdi
  • 90,542
  • 19
  • 167
  • 203
  • This should give me enough to chew on in the meantime. Thank you! – RodericDay Aug 31 '12 at 19:34
  • I was hesitant cause the answer I liked the most is using QGraphicsScene, but this solution is a better answer to the exact question I asked. Here you go! – RodericDay Sep 03 '12 at 01:56
  • I didn't realize you even wanted a QGraphicsScene solution. Your initial example shows a QPushButton. I could have given a QGraphics version if I had known. Its actually not much different. You are just using QGraphicsScene Events instead, and using scene positions instead of global positions – jdi Sep 03 '12 at 02:26
  • Yeah which I realize. I messed up by trying to think too far ahead/phrase the question in a way that I thought was most conducive to an answer (didn't even know about QGraphicsScene) and then I was stuck with the awkward decision of how to best help a programmer with my question in the future. Hopefully this little discussion we had here will settle it. – RodericDay Sep 03 '12 at 05:31
  • What is `super()` thing doing here? I always thought it was used to initialize the parent class. – Santosh Kumar Oct 23 '18 at 10:02
  • @SantoshKumar it is not solely used for calling the constructor of a parent class. It is used to access any attribute on the parent class. In this example I use it to call the default event handling behaviours if my custom logic does not apply. – jdi Oct 23 '18 at 10:04
  • @jdi I am trying to change the color of the label using setStyleSheet while it is being moved. I do this in the mouseMoveEvent function, but the problem is that when I let go and move another button, the original button moves back to its original spot. I cannot figure out why setStyleSheet would result in this behavior. – Shock-o-lot Dec 06 '19 at 23:04
  • @Shock-o-lot it sounds highly unlikely that using a stylesheet would cause the button to move back to its original location. Are you sure that it the factor? When you disable the setStyleSheet call, you notice it work correctly? – jdi Dec 08 '19 at 05:57
  • @jdi I posted a separate question here: https://stackoverflow.com/questions/59221822/pyside-movable-labels-snap-back-to-original-position-when-released-trying-to-m/59222559#59222559. – Shock-o-lot Dec 09 '19 at 18:20
2

It depends on what you are trying to do. If you are trying to do actual "Drag & Drop", you're going about it wrong. What you are doing is just moving the button around in its X,Y coordinate space within its parent. Its never actually invoking any Drag/Drop events, those are entirely different.

You should read through the drag & drop documentation here:

http://doc.qt.nokia.com/4.7-snapshot/dnd.html
http://qt-project.org/doc/qt-5.0/qdrag.html

Instead of moving the button within the mousePressEvent, you'll need to create a new QDrag object and execute it. You can make it look like your button by taking a snapshot of your button using the QPixmap::grabWidget method and assign it to the QDrag instance using the QDrag::setPixmap method.

Event if all you are trying to do is move the widget around in the parent space, I would recommend using this framework and just accepting the drop event for your the button. Then you don't trigger a bunch of unnecessary redraws.

jdi
  • 90,542
  • 19
  • 167
  • 203
Eric Hulser
  • 3,912
  • 21
  • 20
  • any idea where I could find a nice example to follow? I wouldn't mind starting by dragging a colored rectangle or something. The idea of a "QDrag" instance seems overly complex to me for some reason, as if it's purpose was dragging a folder into a directory or something. – RodericDay Aug 31 '12 at 18:09
  • Yea, that is the main point of it. I guess the question is, is what is your goal? – Eric Hulser Aug 31 '12 at 18:30
  • My ultimate goal is to be able to simulate a game board by importing *.bmps In the short term I want to be able to move QLabel objects containing a Pixmap around, like chess pieces on a board. But without any logic or auto-drop zones. Just images on a screen. The best analogy I can think of is fridge magnets. I want virtual fridge magnets. – RodericDay Aug 31 '12 at 18:53
  • 2
    Ah, ok, then yea, ignore the QDrag stuff - you're going about it with the wrong widget. For what you want to do, you should look into the QGraphicsView framework. See this question & answer here: http://stackoverflow.com/questions/12213391/python-pyqt-how-can-i-move-my-widgets-on-the-window-with-mouse/12219643#12219643 – Eric Hulser Aug 31 '12 at 20:13