Search code examples
pyqt5qgraphicsitemqgraphicspixmapitemqgraphicsellipseitem

QGraphicsPixmapItem is not being positioned correctly


I need to move a QGraphicsPixmapItem through a circle that it is at the top left corner of the image. That is, when I grab with the mouse the circle, I need the top left corner of the image to follow the circle. I subclassed a QGraphicsEllipseItem and reimplemented the itemChange method but when I set the position of the image to that value, the image is not being positioned correctly. What should I modify in my code?

    import sys
    from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsView
    from PyQt5 import QtGui, QtWidgets
    
    class MainWindow(QMainWindow):
        def __init__(self, parent=None):
            super(MainWindow, self).__init__(parent)
    
            self.scene = Scene()
            self.view = QGraphicsView(self)
            self.setGeometry(10, 30, 850, 600)
            self.view.setGeometry(20, 22, 800, 550)
            self.view.setScene(self.scene)
    
    class Scene(QtWidgets.QGraphicsScene):
        def __init__(self, parent=None):
            super(Scene, self).__init__(parent)
            # other stuff here
            self.set_image()
    
        def set_image(self):
            image = Image()
            self.addItem(image)
            image.set_pixmap()
    
    class Image(QtWidgets.QGraphicsPixmapItem):
        def __init__(self, parent=None):
            super(Image, self).__init__(parent)
    
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
    
        def set_pixmap(self):
            pixmap = QtGui.QPixmap("image.jpg")
            self.setPixmap(pixmap)
            self.pixmap_controller = PixmapController(self)
            self.pixmap_controller.set_pixmap_controller()
            self.pixmap_controller.setPos(self.boundingRect().topLeft())
            self.pixmap_controller.setFlag(QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges, True)
    
        def change_image_position(self, position):
            self.setPos(position)
    
    class PixmapController(QtWidgets.QGraphicsEllipseItem):
        def __init__(self, pixmap):
            super(PixmapController, self).__init__(parent=pixmap)
            self.pixmap = pixmap
    
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
            color = QtGui.QColor(0, 0, 0)
            brush = QtGui.QBrush(color)
            self.setBrush(brush)
    
        def set_pixmap_controller(self):
            self.setRect(-5, -5, 10, 10)
    
        def itemChange(self, change, value):
            if change == QtWidgets.QGraphicsItem.ItemPositionChange:
                self.pixmap.change_image_position(value)
            return super(PixmapController, self).itemChange(change, value)
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        window = MainWindow()
        window.show()
        sys.exit(app.exec_())

Solution

  • When a graphics item has a parent, its coordinate system is based on that parent, not on the scene.

    The problem is that when you try to move the PixmapController, the movement is in parent coordinates (the pixmap item). When you check for the ItemPositionChange you are you're changing the parent position but the item position is changed anyway, based on the parent coordinate system.

    While you could just return an empty QPoint (which will not change the item position), this wouldn't be a good choice: as soon as you release the mouse and start to move it again, the pixmap will reset its position.

    The solution is not to set the movable item flag, but filter for mouse movements, compute a delta based on the click starting position, and use that delta to move the parent item based on its current position.

    class PixmapController(QtWidgets.QGraphicsEllipseItem):
        def __init__(self, pixmap):
            super(PixmapController, self).__init__(parent=pixmap)
            self.pixmap = pixmap
    
            # the item should *NOT* move
            # self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
            color = QtGui.QColor(0, 0, 0)
            brush = QtGui.QBrush(color)
            self.setBrush(brush)
    
        def set_pixmap_controller(self):
            self.setRect(-5, -5, 10, 10)
    
        def mousePressEvent(self, event):
            if event.button() == QtCore.Qt.LeftButton:
                self.startPos = event.pos()
    
        def mouseMoveEvent(self, event):
            if event.buttons() == QtCore.Qt.LeftButton:
                delta = event.pos() - self.startPos
                self.parentItem().setPos(self.parentItem().pos() + delta)
    

    If you want to use your change_image_position function, you need to change those functions accordingly; the code below does the same thing as the last line in the example above:

    class Image(QtWidgets.QGraphicsPixmapItem):
        # ...
        def change_image_position(self, delta):
            self.setPos(self.pos() + delta)
    
    class PixmapController(QtWidgets.QGraphicsEllipseItem):
        # ...
        def mouseMoveEvent(self, event):
            if event.buttons() == QtCore.Qt.LeftButton:
                delta = event.pos() - self.startPos
                self.pixmap.change_image_position(delta)
    

    Tip: do not add a child widget to a QMainWindow like that, as it will not resize correctly when the window is resized. Use self.setCentralWidget(self.view) instead; if you want to add margins, use a container QWidget, set that widget as the central widget, add a simple QHBoxLayout (or QVBoxLayout), add the view to that layout and then set the margins with layout.setContentsMargins(left, top, right, bottom)