PyQt5 - Ignoring widgets within QGraphicsView when dragging and dropping

65 views Asked by At

I have a test PyQt application with a QGraphicsView/Scene, and a toolbar with a button in it which allows one to drag a shape into the viewport.

There's also another button in the middle of the viewport (in my real application this is a number of icons and labels, imagine something like the icons overlaid at the top of unreal engine viewport UI).

923e2b9e-3b15-4dca-b1ec-9904e10d9bba-image.png

When dragging a shape from the toolbar into the viewport, if somebody mouses over the button embedded in the viewport, it triggers a 'dragLeaveEvent' method, where I actually want to ignore this button entirely, letting me continue to drag the shape around the viewport.

I have tried various things, including handling the dragging and dropping from the scene, from the view, and setting event.ignore() on the default drag/drop methods in various places. If I ignore events on the QGraphicsView, then I can't drag/drop on the scene either. It seems whatever's on top will decide what the drag/drop functionality will be. I really want to be able to ignore the top level widget (the viewport button).

Here is minimal code for the issue. If you drag the 'circle' from the toolbar onto the viewport, and hover over the viewport button, the circle will drop to the scene, and a new one will be created. This is not the behaviour I'm looking for. I want to be able to ignore the viewport button and continue to drag the object around.

Any advice would be most appreciated!

Cheers, Ekar

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsScene, QGraphicsView, QGraphicsItem, QToolBar, QPushButton, QVBoxLayout, QWidget, QLabel, QGraphicsEllipseItem
from PyQt5.QtGui import QDrag
from PyQt5.QtCore import Qt, QMimeData, QRectF, QPointF


class CircleItem(QGraphicsItem):
    def __init__(self, x, y, radius, parent=None):
        super().__init__(parent)

        # create a simple shape we can move around the scene
        self.shape = QGraphicsEllipseItem(x, y, radius, radius)

    def boundingRect(self):
        return QRectF(self.shape.boundingRect())

    def paint(self, painter, option, widget=None):
        self.shape.paint(painter, option, widget)


# for toolbar icons
class DragDropWidget(QLabel):
    def __init__(self, shape_type):
        super().__init__(shape_type)

        self.shape_type = shape_type

    def mousePressEvent(self, event):
        drag = QDrag(self)
        drag.setHotSpot(event.pos())
        mime_data = QMimeData()
        mime_data.setText(self.shape_type)
        drag.setMimeData(mime_data)
        drag.exec_(Qt.MoveAction)


class CustomView(QGraphicsView):
    def __init__(self, scene):
        super(CustomView, self).__init__(scene)

    # def dragEnterEvent(self, event):
    #     event.ignore()
        
    # def dragMoveEvent(self, event):
    #     event.ignore()

    # def dropEvent(self, event):
    #     event.ignore()

    # def dragLeaveEvent(self, event):
    #     event.ignore()


class CustomScene(QGraphicsScene):
    def __init__(self):
        super(CustomScene, self).__init__()

        self.shape = None

    def dragEnterEvent(self, event):
        if event.mimeData().hasText():
            # Create the shape and add to scene
            shape_type = event.mimeData().text()
            self.shape = CircleItem(0, 0, 100)
            self.addItem(self.shape)

        event.acceptProposedAction()

    def dragMoveEvent(self, event):
        if self.shape:
            # Move the shape around the scene with mouse cursor
            self.shape.setPos(event.scenePos())

        event.acceptProposedAction()

    def dropEvent(self, event):
        # Drop the shape to the scene
        self.shape = None
        event.acceptProposedAction()

    def dragLeaveEvent(self, event):
        # I don't want this event to be tiggered when mousing over button in viewport
        print("LEAVING!")
        #event.acceptProposedAction()




class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.scene = CustomScene()
        self.view = CustomView(self.scene)
        self.setCentralWidget(self.view)

        toolbar = QToolBar("Shapes Toolbar")
        self.addToolBar(toolbar)

        circle_button = DragDropWidget('Circle')
        toolbar.addWidget(circle_button)

        layout = QVBoxLayout()
        layout.addWidget(self.view)
        main_widget = QWidget()
        main_widget.setLayout(layout)
        self.setCentralWidget(main_widget)

        # UI for the view
        view_button = QPushButton('I want to ignore this button when drag/dropping')
        view_layout = QVBoxLayout()
        view_layout.addWidget(view_button)
        self.view.setLayout(view_layout)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.setGeometry(100, 100, 800, 600)
    mainWindow.show()
    sys.exit(app.exec_())


1

There are 1 answers

2
user2407089 On

I'm updating my answer here based on musicamante's latest comment, which advises not to set a layout on the viewport, but to create the 'viewport_controls' with the viewport as parent, and then handle the resizing from the parent widget. I have cut down the code to a more simplified example.

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsScene, QGraphicsView, QToolBar, QPushButton, QHBoxLayout, QWidget, QLabel


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

        self.parent = parent

        self.layout = QHBoxLayout(self)
        self.button = QPushButton('Do something')
        self.button2 = QPushButton('Do something else')
        self.layout.addWidget(self.button)
        self.layout.addWidget(self.button2)


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.scene = QGraphicsScene()
        self.view = QGraphicsView(self.scene)
        self.setCentralWidget(self.view)

        toolbar = QToolBar("Shapes Toolbar")
        self.addToolBar(toolbar)

        circle_button = QLabel('Circle')
        toolbar.addWidget(circle_button)

        self.viewport_controls = ViewportControls(self.view.viewport())

    def resizeEvent(self, event):
        new_size = self.size()  # Get the new size of the widget
        self.viewport_controls.setGeometry(0, 0, new_size.width(), new_size.height())
        super().resizeEvent(event)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.setGeometry(100, 100, 800, 600)
    mainWindow.show()
    sys.exit(app.exec_())