Search code examples
pyqtpyqt5qt-designer

PyQt creating color circle


I want to add a color circle to the widget placeholder there: enter image description here

I already tried this library: https://gist.github.com/tobi08151405/7b0a8151c9df1a41a87c1559dac1243a

But if the window wasnt a quadrat, the color circle didnt worked. I have already tried the other solution, but there is no method to get the color value. How to create a "Color Circle" in PyQt?

Could you recommend/ show me a way of creating my own, so I can add them there? Thanks!


Solution

  • The widget assumes that its shape is always a square; the code provides a custom AspectLayout for that, but it's not necessary.

    The problem comes from the fact that when the shape is not a square the computation of the color is wrong, as coordinates are not properly mapped when a dimension is much bigger than the other. For instance, if the widget is much wider than tall, the x coordinate is "shifted" since the circle (which is now an actual ellipse) is shown centered, but the color function uses the minimum size.

    The solution is to create an internal QRect that is always displayed at the center and use that for both painting and computation:

    class ColorCircle(QWidget):
        # ...
        def resizeEvent(self, ev: QResizeEvent) -> None:
            size = min(self.width(), self.height())
            self.radius = size / 2
            self.square = QRect(0, 0, size, size)
            self.square.moveCenter(self.rect().center())
    
        def paintEvent(self, ev: QPaintEvent) -> None:
            # ...
            p.setPen(Qt.transparent)
            p.setBrush(hsv_grad)
            p.drawEllipse(self.square)
            p.setBrush(val_grad)
            p.drawEllipse(self.square)
            # ...
    
        def map_color(self, x: int, y: int) -> QColor:
            x -= self.square.x()
            y -= self.square.y()
            # ...
    

    Note that the code uses numpy, but it's used for functions that do not really require such a huge library for an application that clearly doesn't need numpy's performance.

    For instance, the line_circle_inter uses a complex way to compute the position for the "cursor" (the small circle), but that's absolutely unnecessary, as the Hue and Saturation values already provide "usable" coordinates: the Hue indicates the angle in the circle (starting from 12 hour position, counter-clockwise), while the Saturation is the distance from the center.
    QLineF provides a convenience function, fromPolar(), which returns a line with a given length and angle: the length will be the radius multiplied by the Saturation, the angle is the Hue multiplied by 360 (plus 90°, as angles start always at 3 o'clock); then we can translate that line at the center of the circle and the cursor will be positioned at the second point of the segment:

        def paintEvent(self, event):
            # ...
            p.setPen(Qt.black)
            p.setBrush(self.selected_color)
            line = QLineF.fromPolar(self.radius * self.s, 360 * self.h + 90)
            line.translate(self.square.center())
            p.drawEllipse(line.p2(), 10, 10)
    

    The map color function can use the same logic, but inverted: we construct a line starting from the center to the mouse cursor position, then the Saturation is the length divided by the radius (sanitizing the value to 1.0, as that's the maximum possible value), while the Hue is the line angle (minus 90° as above) divided by 360 and sanitized for a positive 0.0-1.0 range.

        def map_color(self, x: int, y: int) -> QColor:
            line = QLineF(QPointF(self.rect().center()), QPointF(x, y))
            s = min(1.0, line.length() / self.radius)
            h = (line.angle() - 90) / 360 % 1.
            return h, s, self.v