Scaling-Rotating functionality on QGraphicsPixmapItem

81 views Asked by At

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?

1

There are 1 answers

0
Uncle Bob On BEST ANSWER

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_()