3

Consider the following toy example:

from PyQt5 import QtWidgets, QtGui, QtCore

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        w = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout()
        w.setLayout(layout)
        self.setCentralWidget(w)

        label = QtWidgets.QLabel()
        canvas = QtGui.QPixmap(400, 300)
        label.setPixmap(canvas)
        layout.addWidget(label)

        def paintEvent():
            painter = QtGui.QPainter(label.pixmap())
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
            painter.setPen(QtCore.Qt.red)
            painter.drawArc(0, 0, 100, 100, 1440, -2880)
            painter.end()

        paintEvent()

        self.show()

app = QtWidgets.QApplication([])
window = MainWindow()
app.exec_()

How can I paint the arc using an arbitrary number of colours ideally of varying lengths?

I tried to do it with gradients (linear and conical) but I have been unable to obtain accurate results.

I suppose the broader question is can I somehow have different pen colours when painting an arc? Note that the arc can be a half circle, a full circle or anything in between.

The colours are to be distributed using percentages. Each colour is a fraction of the arc's length. But I am content with a solution where all colours are equally spaced.

Giuseppe
  • 143
  • 1
  • 10

2 Answers2

3

A possible solution is to paint the arc in parts:

from PyQt5 import QtCore, QtGui, QtWidgets


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        w = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout()
        w.setLayout(layout)
        self.setCentralWidget(w)

        label = QtWidgets.QLabel()
        canvas = QtGui.QPixmap(400, 300)
        canvas.fill(QtGui.QColor("white"))
        label.setPixmap(canvas)
        layout.addWidget(label)

        def paint_label():
            painter = QtGui.QPainter(label.pixmap())
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
            r = QtCore.QRect(0, 0, 100, 100)
            delta_angle = -180 * 16
            start_angle = 90 * 16
            values = (1, 2, 3, 4)
            colors = (
                QtGui.QColor("red"),
                QtGui.QColor("blue"),
                QtGui.QColor("green"),
                QtGui.QColor("yellow"),
            )
            sum_of_values = sum(values)
            for value, color in zip(values, colors):
                end_angle = start_angle + int((value/sum_of_values) * delta_angle)
                painter.setPen(color)
                painter.drawArc(r, start_angle, end_angle - start_angle)
                start_angle = end_angle
            painter.end()

        paint_label()

        self.show()


def main():

    app = QtWidgets.QApplication([])
    window = MainWindow()
    app.exec_()


if __name__ == "__main__":
    main()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
1

The solution provided by eyllanesc is perfectly fine, but I wanted to show the possibility of achieving the same result using a conical gradient instead of drawing single arcs.

Since we want actual arcs to be drawn, the trick is to use "ranges" of colors with very narrow margins.
For example, to get a conical gradient that is half red and half blue, we'll use something like this:

gradient.setColorAt(.5, QtCore.Qt.red)
# set the next color with a stop very close to the previous
gradient.setColorAt(.500001, QtCore.Qt.blue)

I prepared an example with a small interface to test its possibilities out.

screenshot of the example code

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        w = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout()
        w.setLayout(layout)
        self.setCentralWidget(w)

        panelLayout = QtWidgets.QHBoxLayout()
        layout.addLayout(panelLayout)

        panelLayout.addWidget(QtWidgets.QLabel('Start'))
        self.startSpin = QtWidgets.QSpinBox(maximum=360, suffix='°')
        self.startSpin.setValue(90)
        panelLayout.addWidget(self.startSpin)
        panelLayout.addWidget(QtWidgets.QLabel('Extent'))
        self.extentSpin = QtWidgets.QSpinBox(maximum=360, suffix='°')
        self.extentSpin.setValue(180)
        panelLayout.addWidget(self.extentSpin)
        panelLayout.addWidget(QtWidgets.QLabel('Width'))
        self.penSpin = QtWidgets.QSpinBox(minimum=1, maximum=20, suffix='px')
        self.penSpin.setValue(3)
        panelLayout.addWidget(self.penSpin)

        self.startSpin.valueChanged.connect(self.updateCanvas)
        self.extentSpin.valueChanged.connect(self.updateCanvas)
        self.penSpin.valueChanged.connect(self.updateCanvas)

        self.colors = []
        self.colorSpins = []
        colorLayout = QtWidgets.QHBoxLayout()
        layout.addLayout(colorLayout)
        for color in ('red', 'green', 'blue', 'yellow'):
            colorLayout.addWidget(QtWidgets.QLabel(color))
            self.colors.append(QtGui.QColor(color))
            colorSpin = QtWidgets.QSpinBox(minimum=1, maximum=50, value=25)
            colorLayout.addWidget(colorSpin)
            colorSpin.valueChanged.connect(self.updateCanvas)
            self.colorSpins.append(colorSpin)

        self.label = QtWidgets.QLabel()
        canvas = QtGui.QPixmap(400, 300)
        self.label.setPixmap(canvas)
        layout.addWidget(self.label)

        self.updateCanvas()

        self.show()

    def updateCanvas(self):
        pm = QtGui.QPixmap(self.label.pixmap().size())
        pm.fill(QtCore.Qt.transparent)
        painter = QtGui.QPainter(pm)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.translate(.5, .5)

        sizes = [spin.value() for spin in self.colorSpins]
        total = sum(sizes)
        extent = self.extentSpin.value() / 360
        grad = QtGui.QConicalGradient(50, 50, self.startSpin.value())
        gradPos = 1
        # set colors starting from stop 1.0 to (1.0 - extent), since
        # conical gradients are always counter-clockwise and the actual arc
        # is negative, so it is drawn clockwise
        for i, (size, color) in enumerate(zip(sizes, self.colors)):
            grad.setColorAt(gradPos, color)
            gradPos -= size / total * extent
            if i < len(self.colors) - 1:
                # extend the color right next to the next value
                grad.setColorAt(gradPos + .000001, color)
        if extent != 1:
            # ensure that the first color is not painted at the edget of the
            # last due to antialiasing
            grad.setColorAt(0, self.colors[0])
            grad.setColorAt(1 - extent, self.colors[-1])

        offset = self.penSpin.maximum()
        pen = QtGui.QPen(grad, self.penSpin.value(), cap=QtCore.Qt.FlatCap)
        painter.setPen(pen)
        # move the brush origin so that the conical gradient correctly centered
        # in the middle of the ellipse
        painter.setBrushOrigin(offset, offset)
        painter.drawArc(offset, offset, 100, 100, self.startSpin.value() * 16, -self.extentSpin.value() * 16)
        painter.end()

        self.label.setPixmap(pm)
musicamante
  • 41,230
  • 6
  • 33
  • 58