Search code examples
pythonpyqt5qpainter

How to draw custom shape in Qt by using\combining many shapes


How can I draw a custom shape by using (combining) other basic shapes (Circle and line)?

My problem is that if I want to rotate that custom shape I need to do some math and rotate all other basic shapes and re-positioning. How to avoid that and in one parameter I can rotate the whole of that custom shape.

i want something like this (For example):

orientation_angale = 40 # i.e
custom_shape.rotate(orientation_angale)

Here is the code that draws the above image:

from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtCore import QPoint, QPointF

class CustomShape(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.pixmap = QtGui.QPixmap()

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        painter.drawPixmap(self.rect(), self.pixmap)
        pen = QtGui.QPen()
        pen.setWidth(1)
        painter.setPen(pen)
        # =========== Draw big circle ===========
        x, y = 100, 100
        r = 150
        painter.drawEllipse(x, y, r, r)

        # =========== Draw top line ===========
        s_point_x = x + r / 2  # centre of the big circle
        s_point_y = y
        s_point = QPointF(s_point_x, s_point_y)

        e_point_x = x + r / 2  # centre of the big circle
        e_point_y = y - 20
        e_point = QPointF(e_point_x, e_point_y)

        painter.drawLine(s_point, e_point)

        # =========== Draw small circle ===========
        s_r = 10
        s_x, s_y = x + (r / 2) - (s_r / 2), e_point_y - s_r
        painter.drawEllipse(s_x, s_y, s_r, s_r)


if __name__ == '__main__':
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = CustomShape()
    w.resize(400, 400)
    w.show()
    sys.exit(app.exec_())

here is the default orientation: enter image description here

I want to rotate that custom shape, so it should look like this (the rotation will applied in 360degrees)

enter image description here


Solution

  • You can use the painter.rotate() function, but remember that rotation is always around the origin point (0, 0), so you first have to translate to the center of the rotation, then rotate, and translate back.

    Consider that you can use QPainterPath to "store" advanced shapes in a single element (an alternative is to use QPicture which allows to "cache" QPainter actions).

    class CustomShape(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            self.pixmap = QtGui.QPixmap()
            self.setMinimumSize(210, 210)
    
            self.path = QtGui.QPainterPath()
            self.path.addEllipse(100, 100, 150, 150)
            self.path.moveTo(175, 100)
            self.path.lineTo(175, 80)
            self.path.addEllipse(170, 70, 10, 10)
            self.center = QtCore.QPoint(175, 175)
            self.angle = 0
    
        def setAngle(self, angle):
            self.angle = angle % 360
            self.update()
    
        def paintEvent(self, event):
            painter = QtGui.QPainter(self)
            painter.drawPixmap(self.rect(), self.pixmap)
            painter.setRenderHint(painter.Antialiasing)
            painter.translate(self.center)
            painter.rotate(self.angle)
            painter.translate(-self.center)
            painter.drawPath(self.path)
    
    if __name__ == '__main__':
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        w = CustomShape()
        w.resize(400, 400)
        timer = QtCore.QTimer(interval=100)
        timer.timeout.connect(lambda: w.setAngle(w.angle + 1))
        timer.start()
        w.show()
        sys.exit(app.exec_())
    

    Be aware that Qt provides helper functions that can be used to do computations for simple geometries and positions; while not always optimal for frequent updates, since it relies on C++ functions it's usually faster than computing everything on the python side, or, at least, it provides a better readable form:

    class CustomShape(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            self.pixmap = QtGui.QPixmap()
            self.setMinimumSize(210, 210)
    
            self.center = QtCore.QPoint(175, 175)
            self.bigRadius = 75
            self.smallRadius = 5
            self.distance = 20
            self.angle = 0
    
        def setAngle(self, angle):
            self.angle = angle % 360
            self.update()
    
        def paintEvent(self, event):
            painter = QtGui.QPainter(self)
            painter.drawPixmap(self.rect(), self.pixmap)
            painter.setRenderHint(painter.Antialiasing)
            painter.drawEllipse(self.center, self.bigRadius, self.bigRadius)
    
            fullExtent = self.bigRadius + self.distance
            # create a line that extents to the edge of the small circle
            line = QtCore.QLineF.fromPolar(fullExtent, 90 - self.angle)
            # translate it to the center
            line.translate(self.center)
            # move the origin point based on the ratio of the radius
            line.setP1(line.pointAt(self.bigRadius / fullExtent))
            painter.drawLine(line)
    
            # create a line similar to the above to get the center of the 
            # small circle
            otherLine = QtCore.QLineF.fromPolar(
                fullExtent + self.smallRadius, 90 - self.angle)
            otherLine.translate(self.center)
            painter.drawEllipse(otherLine.p2(), self.smallRadius, self.smallRadius)