pyqt5qgraphicsviewqgraphicssceneqgraphicspixmapitem

Scaling-Rotating functionality on QGraphicsPixmapItem


I searched many times over the internet if QGraphicView have functionality for rotating/scaling image and I had no success.

What I want like every diagram program each image/shape has boundary points, so the user can scale or rotate the shape/image. like below:

enter image description here

And what I have so far is this:

import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *


class ImagePoint(QGraphicsRectItem):

    def __init__(self, x, y, w=15, h=15, parent=None):
        super(ImagePoint, self).__init__(x - w / 2, y - w / 2, w, h, parent)
        self.setAcceptHoverEvents(True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
        self.setAcceptHoverEvents(True)
        self.setBrush(QColor(Qt.blue))

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemPositionChange:
            self.parentItem().setScale(value)
        super(ImagePoint, self).itemChange(change, value)


class Back(QGraphicsPixmapItem):

    def __init__(self, file_name, scene):
        super(Back, self).__init__()
        self.setAcceptHoverEvents(True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
        self.setAcceptHoverEvents(True)

        pixmap = QPixmap(file_name)
        self.scene = scene
        self.setPixmap(pixmap)
        self.init_boundre_points()

    def init_boundre_points(self):
        ImagePoint(self.boundingRect().topLeft().x(), self.boundingRect().topLeft().y(), parent=self)
        ImagePoint(self.boundingRect().topRight().x(), self.boundingRect().topRight().y(), parent=self)
        ImagePoint(self.boundingRect().bottomLeft().x(), self.boundingRect().bottomLeft().y(), parent=self)
        ImagePoint(self.boundingRect().bottomRight().x(), self.boundingRect().bottomRight().y(), parent=self)

        x = self.boundingRect().topLeft().x() + self.boundingRect().width() / 2
        y = self.boundingRect().topLeft().y()
        ImagePoint(x, y, parent=self)

        x = self.boundingRect().bottomLeft().x() + self.boundingRect().width() / 2
        y = self.boundingRect().bottomLeft().y()
        ImagePoint(x, y, parent=self)

        x = self.boundingRect().topLeft().x()
        y = self.boundingRect().topLeft().y() + self.boundingRect().height() / 2
        ImagePoint(x, y, parent=self)

        x = self.boundingRect().topRight().x()
        y = self.boundingRect().topRight().y() + self.boundingRect().height() / 2
        ImagePoint(x, y, parent=self)


class MyGraphicsView(QGraphicsView):
    def __init__(self):
        super(MyGraphicsView, self).__init__()
        self.setScene(MyGraphicsScene(self))


class MyGraphicsScene(QGraphicsScene):
    def __init__(self, parent):
        super(MyGraphicsScene, self).__init__()
        self.setBackgroundBrush(QBrush(QColor(50, 50, 50)))
        back = Back("Path_image", self)
        self.addItem(back)


class MyMainWindow(QMainWindow):
    def __init__(self):
        super(MyMainWindow, self).__init__()
        self.setWindowTitle("Test")
        self.resize(800, 600)
        self.gv = MyGraphicsView()
        self.setCentralWidget(self.gv)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MyMainWindow()
    ex.show()
    sys.exit(app.exec_())

So how can effectively do that? is there any thing I missed on QGraphicView framework?


Solution

  • Well, I've implemented it as the following:

    1. creating QGraphicRectItem and adding it to the scene.
    2. Adding QGraphicPixmapItem as a child of RectItem. 3, Draw adjustment points.
    3. Get the edges of the RectItem, and handle the resizing in mouseMoveEvent()

    Here is the code:

    import sys
    from PyQt5.QtGui import QPen, QBrush, QColor, QPixmap
    from PyQt5.QtCore import Qt, QPointF
    from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsRectItem, QMainWindow, \
        QVBoxLayout, QWidget, QGraphicsSimpleTextItem, QGraphicsPixmapItem
    
    
    class ResizableRect(QGraphicsRectItem):
        selected_edge = None
    
        def __init__(self, x, y, width, height):
            super().__init__(0, 0, width, height)
            self.setPos(x, y)
            self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
            self.setAcceptHoverEvents(True)
            self.setPen(QPen(QBrush(Qt.blue), 5))
    
            self.fileName = "C:/Users/USER/Desktop/inactive_tap.png"
            self.pix = QPixmap(self.fileName)
            self.pix = self.pix.scaled(width, height, transformMode=Qt.SmoothTransformation)
            self.pixItem = QGraphicsPixmapItem(self.pix, self)
    
            self.init_boundre_points()
    
        def getEdges(self, pos):
            edges = Qt.Edges()
            rect = self.rect()
            border = self.pen().width() / 2
    
            if pos.x() < rect.x() + border:
                edges |= Qt.LeftEdge
            elif pos.x() > rect.right() - border:
                edges |= Qt.RightEdge
            if pos.y() < rect.y() + border:
                edges |= Qt.TopEdge
            elif pos.y() > rect.bottom() - border:
                edges |= Qt.BottomEdge
    
            return edges
    
        def mousePressEvent(self, event):
            if event.button() == Qt.LeftButton:
                self.selected_edge = self.getEdges(event.pos())
                self.offset = QPointF()
            else:
                self.selected_edge = Qt.Edges()
            super().mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            if self.selected_edge:
                mouse_delta = event.pos() - event.buttonDownPos(Qt.LeftButton)
                rect = self.rect()
                pos_delta = QPointF()
                border = self.pen().width()
    
                if self.selected_edge & Qt.LeftEdge:
                    diff = min(mouse_delta.x() - self.offset.x(), rect.width() - border)
                    if rect.x() < 0:
                        offset = diff / 2
                        self.offset.setX(self.offset.x() + offset)
                        pos_delta.setX(offset)
                        rect.adjust(offset, 0, -offset, 0)
                    else:
                        pos_delta.setX(diff)
                        rect.setWidth(rect.width() - diff)
    
                elif self.selected_edge & Qt.RightEdge:
                    if rect.x() < 0:
                        diff = max(mouse_delta.x() - self.offset.x(), border - rect.width())
                        offset = diff / 2
                        self.offset.setX(self.offset.x() + offset)
                        pos_delta.setX(offset)
                        rect.adjust(-offset, 0, offset, 0)
                    else:
                        rect.setWidth(max(border, event.pos().x() - rect.x()))
    
                if self.selected_edge & Qt.TopEdge:
                    diff = min(mouse_delta.y() - self.offset.y(), rect.height() - border)
                    if rect.y() < 0:
                        offset = diff / 2
                        self.offset.setY(self.offset.y() + offset)
                        pos_delta.setY(offset)
                        rect.adjust(0, offset, 0, -offset)
                    else:
                        pos_delta.setY(diff)
                        rect.setHeight(rect.height() - diff)
    
                elif self.selected_edge & Qt.BottomEdge:
                    if rect.y() < 0:
                        diff = max(mouse_delta.y() - self.offset.y(), border - rect.height())
                        offset = diff / 2
                        self.offset.setY(self.offset.y() + offset)
                        pos_delta.setY(offset)
                        rect.adjust(0, -offset, 0, offset)
                    else:
                        rect.setHeight(max(border, event.pos().y() - rect.y()))
    
                #  re-init Pixmap so the quality of the image will stay good.
                self.pix = QPixmap(self.fileName)
                self.pix = self.pix.scaled(self.rect().width(), self.rect().height(), transformMode=Qt.SmoothTransformation)
                self.pixItem.setPixmap(self.pix)
    
                # re-draw the adjustments points  after scaling
                self.init_boundre_points()
                if rect != self.rect():
                    self.setRect(rect)
                    if pos_delta:
                        self.setPos(self.pos() + pos_delta)
            else:
                # use the default implementation for ItemIsMovable
                super().mouseMoveEvent(event)
    
        def mouseReleaseEvent(self, event):
            self.selected_edge = Qt.Edges()
            super().mouseReleaseEvent(event)
    
        def hoverMoveEvent(self, event):
            edges = self.getEdges(event.pos())
            if not edges:
                self.unsetCursor()
            elif edges in (Qt.TopEdge | Qt.LeftEdge, Qt.BottomEdge | Qt.RightEdge):
                self.setCursor(Qt.SizeFDiagCursor)
            elif edges in (Qt.BottomEdge | Qt.LeftEdge, Qt.TopEdge | Qt.RightEdge):
                self.setCursor(Qt.SizeBDiagCursor)
            elif edges in (Qt.LeftEdge, Qt.RightEdge):
                self.setCursor(Qt.SizeHorCursor)
            else:
                self.setCursor(Qt.SizeVerCursor)
    
        def init_boundre_points(self):
            for item in self.childItems():
                if isinstance(item, ImagePoint):
                    self.scene().removeItem(item)
                    del item
            ImagePoint(self.boundingRect().topLeft().x(), self.boundingRect().topLeft().y(), parent=self)
            ImagePoint(self.boundingRect().topRight().x(), self.boundingRect().topRight().y(), parent=self)
            ImagePoint(self.boundingRect().bottomLeft().x(), self.boundingRect().bottomLeft().y(), parent=self)
            ImagePoint(self.boundingRect().bottomRight().x(), self.boundingRect().bottomRight().y(), parent=self)
    
            x = self.boundingRect().topLeft().x() + self.boundingRect().width() / 2
            y = self.boundingRect().topLeft().y()
            self.topEdge = ImagePoint(x, y, parent=self)
    
            x = self.boundingRect().bottomLeft().x() + self.boundingRect().width() / 2
            y = self.boundingRect().bottomLeft().y()
            self.bottomEdge = ImagePoint(x, y, parent=self)
    
            x = self.boundingRect().topLeft().x()
            y = self.boundingRect().topLeft().y() + self.boundingRect().height() / 2
            self.rightEdge = ImagePoint(x, y, parent=self)
    
            x = self.boundingRect().topRight().x()
            y = self.boundingRect().topRight().y() + self.boundingRect().height() / 2
            self.leftEdge = ImagePoint(x, y, parent=self)
    
    
    class ImagePoint(QGraphicsRectItem):
    
        def __init__(self, x, y, w=5, h=5, parent=None):
            super(ImagePoint, self).__init__(x - w / 2, y - w / 2, w, h, parent)
            self.setBrush(QColor(Qt.white))
            self.setPen(QColor("blue"))
            self.setAcceptHoverEvents(False)
    
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
    
            scene = QGraphicsScene(0, 0, 300, 300)
            self.view = QGraphicsView(scene)
    
            self.rect = ResizableRect(0, 50, 200, 100)
            scene.addItem(self.rect)
    
            central = QWidget()
    
            layout = QVBoxLayout(central)
            layout.addWidget(self.view)
    
            self.setCentralWidget(central)
    
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        window = MainWindow()
        window.show()
    
        app.exec_()