pythonpyqt5qt5qdockwidgetqtoolbar

Move widget with cursor to another layout


I want to simulate QDockWidget/QToolBar behavior for custom widget, unpin widget with mousePressEvent from one location and pin it to another location. Like place upper frame to 'sub' location:

enter image description here

I'm able to unpin it with the following code:

class DraggableSpectralFrame(SpectralFrame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMouseTracking(True)
        self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
        self.oldPos = QPointF(self.pos())
        self.local= QPointF(self.pos())
        self.floating = False

    def mousePressEvent(self, event):
        self.oldPos = event.globalPos()
        if self.hoverRect.contains(event.localPos()) and \
                event.button() == Qt.LeftButton:
            self.floating = True
            self.local = event.localPos()
            self.setParent(None)
            self.show()
            self.move((self.oldPos - self.local).toPoint())


    def mouseMoveEvent(self, event):
        self.oldPos = event.globalPos()
        if self.floating:
            self.move((self.oldPos - self.local).toPoint())

enter image description here

The problem that I'm loosing pressed functionality after morphing DraggableSpectralFrame into window and to move window in a proper way I have to click/press again. Without extra click window still reacts on cursors moves(due to self.floating being True) but easily looses focus on sharp movements.

May be there is an easier way to implement that idea, if that is the case - please let me know!


Solution

  • One way to implement dragging and dropping is to use a QDrag object to display the widget while dragging. For the widget that should accept the dragged widget you need to set acceptDrops to True and reimplement at the very least dragEnterEvent and dropEvent. Here is a very simple example how this can be done. In this example the "Move me" label can be dragged back and forth between the two windows.

    from PyQt5 import QtWidgets, QtCore, QtGui
    
    
    class DropFrame(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.vlayout = QtWidgets.QVBoxLayout(self)
            self.setAcceptDrops(True)
    
        def dragEnterEvent(self, event):
            if isinstance(event.source(), DraggableLabel):
                event.accept()
    
        def dropEvent(self, event):
            event.accept()
            widget = event.source()
            if widget:
                self.vlayout.addWidget(widget)
    
    
    class DraggableLabel(QtWidgets.QLabel):
        def __init__(self, text, parent=None):
            super().__init__(text, parent)
            self.setFrameShape(self.Box)
    
        def mousePressEvent(self, event):
            self.drag = QtGui.QDrag(self)
            self.mime_data = QtCore.QMimeData()
            self.drag.setMimeData(self.mime_data)
            # capture image of self to use as pixmap while dragging
            self.pixmap = self.grab()
            self.drag.setPixmap(self.pixmap)
    
            self.hide()
            self.drag.exec(QtCore.Qt.MoveAction)
            self.show()
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication([])
        widget1 = DropFrame()
        label = DraggableLabel('Move me')
        widget1.layout().addWidget(label)
        widget1.show()
    
        widget2 = DropFrame()
        widget2.show()
        widget2.move(widget1.pos().x(),widget1.pos().y()+150)
        app.exec()