Search code examples
pythonpyqtpysideqgraphicsitem

How to resize polygon by dragging its corners?


I save points upon clicking mouse on a QGraphicsScene, append it on a list, and create a QGraphicsPolygonItem by looping through the list.

I use a custom QGraphicsScene which emits position whenever there is a mouse press event. The code is as below. (Credits for SignalHelper class)

class SignalHelper(QObject):
    messageSignal = QtCore.Signal(object)

class Scene(QGraphicsScene):
    def __init__(self, parent=None):
        super(Scene, self).__init__(parent)
        self.helper = SignalHelper()

    def mousePressEvent(self, event):
        self.helper.messageSignal.emit(event.scenePos())

Here is the code for my main window.

class main_window(QWidget):
    def __init__(self):
        super().__init__()

        self.setGeometry(100, 100, 500, 500)
        self.pen = QtGui.QPen(QtGui.QColor(0,0,0))                     
        self.pen.setWidth(5)                                             

        self.view = QGraphicsView(self)
        self.scene = Scene()
        self.btn_record_points = QPushButton("Record")
        self.btn_finished = QPushButton("Finished")
        
        self.view.setSceneRect(0, 0, 500,500)
        self.view.setScene(self.scene)

        vbox = QVBoxLayout(self)
        vbox.addWidget(self.view)
        vbox.addWidget(self.btn_record_points)
        vbox.addWidget(self.btn_finished)

        self.setLayout(vbox)

        self.point_list = []
        self.record_points = False

        #Signals
        self.btn_record_points.clicked.connect(self.enable_record_points)
        self.btn_finished.clicked.connect(self.create_polygon)
        self.scene.helper.messageSignal.connect(self.draw_points)
        
    def create_polygon(self):
        # Remove ellipses
        drawn_points = self.scene.items()
        for i in drawn_points:
            self.scene.removeItem(i)

        polygon = QGraphicsPolygonItem(QtGui.QPolygonF(self.point_list))
        polygon.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.scene.addItem(polygon)
        self.record_points = False
        self.point_list.clear()

    QtCore.Slot(QtCore.QPointF)
    def draw_points(self, point):
        if self.record_points == True:
            self.point_list.append(point)
            self.scene.addEllipse(point.toTuple()[0], point.toTuple()[1], 1, 1) 

    QtCore.Slot(bool)
    def enable_record_points(self):
        self.record_points = True

Here is an example of a random Polygon:

GUI

Note on how to use:

  1. Press "Record" button
  2. Click a number of times on the Scene
  3. Press "Finsihed" button

Now that the Polygon is created, is there a way to resize the Polygon by dragging its corners?


Solution

  • By "resize the Polygon dragging its corners" I'm imagining you want to move the polygon's points around.

    So here is one approach.

    So, first make your polygon selectable, once the polygon is selected iterate through the polygon's points and draw an ellipse at each point. Store the ellipse, polygon_point_index mapping.

    Now check which ellipse is selected by iterating through the mapping_list, then use this ellipse, polygon_index mapping to update the specific point of the polygon.

    here is an example (which you may improve):

    import sys
    from PyQt5 import QtWidgets
    from PyQt5 import QtGui
    from PyQt5 import QtCore
    
    
    class Scene(QtWidgets.QGraphicsScene):
    
        def __init__(self, *args, **kwargs):
            super(Scene, self).__init__(*args, **kwargs)
            self.record_points = True
            self.selected = None  # the selected polygon
            self.points_lst = []  # points that are stored when recording
            self.corner_points = []  # This contains corner point and control point mapping
            self.selected_corner = None
            self.poly_points = [] # points that are stored when resizing (You could instead reuse points_lst)
    
        def record(self):
            self.record_points = True
    
        def removeControlPoints(self):
            """ removes the control points (i,e the ellipse)"""
            for ellipse, _ in self.corner_points:
                self.removeItem(ellipse)
    
            self.corner_points = []
    
        def mousePressEvent(self, event):
            super(Scene, self).mousePressEvent(event)
    
            if self.record_points:
                self.points_lst.append(event.scenePos())
                return
    
            for point in self.corner_points:
                if point[0].contains(event.scenePos()):
                    self.selected_corner = point
                    return
    
            if self.selectedItems():
    
                self.removeControlPoints()
    
                self.selected = self.selectedItems()[0]
                self.poly_points = [self.selected.mapToScene(x) for x in self.selected.polygon()]
    
                for index, point in enumerate(self.poly_points):
                    x, y = point.x(), point.y()
                    ellipse = self.addEllipse(QtCore.QRectF(x - 5, y - 5, 10, 10), brush=QtGui.QBrush(QtGui.QColor("red")))
                    ellipse.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable)
    
                    self.corner_points.append((ellipse, index))
    
            else:
                self.selected = None
                self.removeControlPoints()
                self.corner_points = []
                self.poly_points = []
                self.selected_corner = None
    
        def mouseMoveEvent(self, event) -> None:
            super(Scene, self).mouseMoveEvent(event)
    
            if self.selected_corner:
                self.poly_points[self.selected_corner[1]] = QtCore.QPointF(event.scenePos())
                self.selected.setPolygon(QtGui.QPolygonF(self.poly_points))
    
        def mouseReleaseEvent(self, event) -> None:
            super(Scene, self).mouseReleaseEvent(event)
            self.selected_corner = None
    
        def addPoints(self):  # adds the polygon to the scene
            self.record_points = False
            polygon = self.addPolygon(QtGui.QPolygonF(self.points_lst))
            polygon.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable)
            self.points_lst = []
    
    
    class MainWindow(QtWidgets.QWidget):
    
        def __init__(self, *args, **kwargs):
            super(MainWindow, self).__init__(*args, **kwargs)
    
            self.setLayout(QtWidgets.QVBoxLayout())
    
            view = QtWidgets.QGraphicsView()
            scene = Scene()
            view.setScene(scene)
    
            record_btn = QtWidgets.QPushButton(text="Record", clicked=scene.record)
            finish_btn = QtWidgets.QPushButton(text="Finish", clicked=scene.addPoints)
    
            self.layout().addWidget(view)
            self.layout().addWidget(record_btn)
            self.layout().addWidget(finish_btn)
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
    
        win = MainWindow()
        win.show()
    
        sys.exit(app.exec_())
    

    output:

    enter image description here