Issue
I am trying to color a circle(QGraphicsEllipseItem) 25% blue, 25% transparent, 25% red, 25% transparent inside QGraphicsScene. I am struggling to do so. I tried to do something like this:
class CircleItem(QGraphicsEllipseItem):
def __init__(self, x=0, y=0, radius=0):
super(CircleItem, self).__init__(x - radius, y - radius, radius * 2, radius * 2)
self.setPen(QPen(Qt.red, 2))
gradient = QRadialGradient(self.boundingRect().center(), radius)
gradient.setColorAt(0, Qt.blue)
gradient.setColorAt(0.25, Qt.transparent)
gradient.setColorAt(0.5, Qt.red)
gradient.setColorAt(0.75, Qt.transparent)
self.setBrush(QBrush(gradient))
but it doesn't really fill anything, is there anyway to paint circle the way i want it? Like on picture
Solution
You are using the wrong gradient type.
QRadialGradient spreads the gradient with different radius values, as also shown in the QGradient documentation:
What you need is a QConicalGradient:
Then, even using the proper gradient type, it will not show "sections" of the circle as you want with the given values: you are using a gradient, so its colors gradually change between its stops.
After changing the gradient type (and using the default 0 angle
value) as with the following:
gradient = QConicalGradient(self.boundingRect().center(), 0)
gradient.setColorAt(0, Qt.blue)
gradient.setColorAt(0.25, Qt.transparent)
gradient.setColorAt(0.5, Qt.red)
gradient.setColorAt(0.75, Qt.transparent)
The result will still not be appropriate, and that's because a gradient uses color interpolation, which is the main purpose of a gradient:
A possible solution is to use narrow values next to color changes, for instance:
gradient = QConicalGradient(self.boundingRect().center(), 0)
gradient.setColorAt(0, Qt.blue)
gradient.setColorAt(0.2499, Qt.blue)
gradient.setColorAt(0.25, Qt.transparent)
gradient.setColorAt(0.4999, Qt.transparent)
gradient.setColorAt(0.5, Qt.red)
gradient.setColorAt(0.7499, Qt.red)
gradient.setColorAt(0.75, Qt.transparent)
This is much better, as there is almost no interpolation:
Still, not enough. Even with the small difference between stops, there can be issues at higher resolutions:
Notice the small artifact on top. This is even more visible if the gradient has a different angle with the following change:
gradient = QConicalGradient(self.boundingRect().center(), 15)
Note the 15
value, which rotates the gradient 15° counter-clockwise.
Even when using antialiasing, the gradient is still aliased:
Note that setting the rotation of the item will not solve the issue.
A much better visual result can only be obtained by custom painting, which involves a QGraphicsEllipseItem subclass that internally sets "slices" and overrides painting on its own.
This approach may seem a bit complex, but it's also required for proper displaying, and allows further extensibility (like multiple colors or different extents for each color).
class SmoothCircleItem(QGraphicsEllipseItem):
_colors = None
_colorAngle = 0
_colorRanges = None
def __init__(self, x=0, y=0, radius=1, colors=None, angle=0):
super().__init__(x - radius, y - radius, radius * 2, radius * 2)
self.setPen(QPen(Qt.red, 2))
if colors is None:
colors = (Qt.blue, Qt.transparent, Qt.red, Qt.transparent)
self.setColors(colors, angle)
def setColors(self, colors, angle=None):
# acceptable values for "colors" are lists or tuples of:
# - QColor, Qt.GlobalColor (or their int values)
# - list/tuples of [color, position]
if isinstance(colors[0], (QColor, Qt.GlobalColor, int)):
# accept an arbitrary list of colors that splits the ellipse
# assigning identical parts to each color
_colors = []
ratio = 1 / len(colors)
pos = 0
for c in colors:
if not isinstance(c, QColor):
c = QColor(c)
_colors.append((c, pos))
pos += ratio
colors = _colors
else:
# accept iterables in the form of [color, pos], with positions
# in real ranges between 0.0 and 1.0
colors = sorted(
[(c, p % 1.) for c, p in colors],
key=lambda cd: cd[1]
)
# create an internal list of colors in the following form:
# [color, start, extent]
first = colors[0]
last = [first[0], first[1] % 1.]
self._colors = [last]
for color, pos in colors[1:]:
last.append((pos - last[-1]))
colorData = [color, pos]
self._colors.append(colorData)
last = colorData
if len(colors) > 1:
last.append(1. + first[1] - last[-1])
else:
last.append(1.)
if self._colorAngle == angle:
self._update()
elif isinstance(angle, (int, float)):
self.setColorAngle(angle)
def setColorAngle(self, angle):
angle = angle % 360
if self._colorAngle != angle:
self._colorAngle = angle
self._update()
def _update(self):
if self._colorAngle:
# adjust the start values based on the angle
realAngle = self._colorAngle / 360
values = [(c, (s + realAngle) % 1, e) for c, s, e in self._colors]
else:
values = self._colors
# update angles using QPainter angles, which are in sixteenths
self._colorRanges = [
(c, int(s * 5760), int(e * 5760)) for c, s, e in values
]
self.update()
def paint(self, qp, opt, widget=None):
rect = self.rect()
qp.save()
qp.setPen(Qt.NoPen)
for color, start, extent in self._colorRanges:
qp.setBrush(color)
qp.drawPie(rect, start, extent)
qp.restore()
super().paint(qp, opt, widget)
def setBrush(self, brush):
grad = brush.gradient()
if grad is None or grad.type() != grad.ConicalGradient:
return
self.setColors([(stop[1], stop[0]) for stop in grad.stops()])
The visual difference is quite clear, from the following comparison (the first is the QGradient based item, the second is the above SmoothCircleItem
with similar values, while the third shows a possible extension with different color data:
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.