Search code examples
pyqtqpainterqpainterpath

PyQt - How to use arcs to achieve a specfic shape outline


I have a shape drawn with QPainter, which I need to be able to update dynamically, from a single variable called 'width'. I have attached an image to hopefully show what I mean...

dynamic circular shape

Basically the shape should have three parts. Taking the existing shape (in blue), I need to split it down the middle and add a horizontal section, who's length is determined by 'width' var. I would like this shape to be a fully enclosed outline, with no breaks or visible joins.

My problem is simply trying to figure out how to use arcs in QPainter. I'm not understanding the difference between 'arcMoveTo', and 'arcTo' and how to get these things to work for me.

Current code of the basic shape is here:

import sys

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


class Canvas(QWidget): 
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setMouseTracking(True)

    def paintEvent(self, event):
        qp = QPainter(self)
        qp.setRenderHint(QPainter.Antialiasing, True)

        pen = QPen(Qt.black, 15)
        pen.setJoinStyle(Qt.MiterJoin)
        qp.setPen(pen)
        qp.setBrush(Qt.blue)

        p = QPainterPath(QPointF(0, 100))
        p.arcTo(25, 25, 150, 150, 205, 309)
        p.lineTo(0, 100)
        qp.drawPath(p)

    def mouseMoveEvent(self, event):
        # show x and y pos on screen
        mp = event.pos()
        p = self.mapToGlobal(mp)
        p.setY(p.y()-60)
        p.setX(p.x()+10)
        QToolTip.showText(p, "x:{}\ny:{}".format(mp.x(), mp.y()))


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    window = Canvas()
    window.show()
    sys.exit(app.exec())

I would very much appreciate if someone could help out on this. I've been making a mess of this shape all night! ;)

Looking forward to any replies,

Cheers! Ekar


Solution

  • The purpose of arcMoveTo is to move the current point to a specific angle within an arc, which may be necessary to start to draw a new path from that point; it is actually a "helper" function that could be done with normal trigonometry.

    You don't need that, since you're going to continue the existing path: arcTo actually draws a new arc, and connects its start point with the previous one.

    You have to split the arc in 3 sections:

    • the one that goes from the end of the first "line" (which is automatically connected to the starting point) to the bottom of the arc (270° - 65°);
    • the one that draws the right part of the arc (from 270° to 90°, which is 180°);
    • the last part that connects the remaining arc to the target point (155° - 90°);

    The connecting lines are automatically drawn because we're using arcTo as explained above.

    This means that you need two rectangles as references for the ellipses: the left one (which is the one you're currently using) and the right one, which is translated by the width.

            baseRect = QRectF(25, 25, 150, 150)
            p = QPainterPath(QPointF(0, 100))
            p.arcTo(baseRect, 205, 65)
            p.arcTo(baseRect.translated(self.width, 0), 270, 180)
            p.arcTo(baseRect, 90, 65)
            p.lineTo(0, 100)
            qp.drawPath(p)
    

    To clarify how this works, I've drawn the reference rectangles (and their full ellipses) in the image below:

    Screenshot showing the components

    This is a comprehensive example that shows an application of the above:

    class Canvas(QWidget): 
        def __init__(self, parent=None):
            super().__init__(parent)
    
            self.setMouseTracking(True)
            self.width = 0
    
        def sizeHint(self):
            return QSize(300, 200)
    
        def setWidth(self, width):
            self.width = width
            self.update()
    
        def paintEvent(self, event):
            qp = QPainter(self)
            qp.setRenderHint(QPainter.Antialiasing, True)
    
            pen = QPen(Qt.black, 15)
            pen.setJoinStyle(Qt.MiterJoin)
            qp.setPen(pen)
            qp.setBrush(Qt.blue)
    
            baseRect = QRectF(25, 25, 150, 150)
            p = QPainterPath(QPointF(0, 100))
            p.arcTo(baseRect, 205, 65)
            p.arcTo(baseRect.translated(self.width, 0), 270, 180)
            p.arcTo(baseRect, 90, 65)
            p.lineTo(0, 100)
            qp.drawPath(p)
    
        def mouseMoveEvent(self, event):
            p = event.globalPos() + QPoint(10, -60)
            QToolTip.showText(p, "x:{}\ny:{}".format(p.x(), p.y()))
    
    
    if __name__ == '__main__':
        import sys
        app = QApplication(sys.argv)
        window = QWidget()
        canvas = Canvas()
        spin = QSpinBox(maximum=200, singleStep=10)
        layout = QVBoxLayout(window)
        layout.addWidget(canvas)
        layout.addWidget(spin)
        spin.valueChanged.connect(canvas.setWidth)
        window.show()
        sys.exit(app.exec())