Issue
I want to draw a border around all objects/shapes within a QGraphicsItem
Paint method. (The green and red circles are part of a separate item, so they don't count in this situation)
I am currently drawing RoundedRects
, but I'm looking for a scalable solution that could also support more complicated objects (stars, bananas, etc); situations where you have objects overlaid on each other.
I have an Item that, when selected, changes the border colour of two RoundedRects
.
I want a solution where only the outline border changes colour, not the internal.
Possible methods I think might work are using a QGraphicsDropShadowEffect
or creating a QtCore.Qt.MaskOutColor
and controlling the line thickness, somehow. I've done this before by scaling up a duplicate masked shape, but the results aren't ideal. So I'd be really interested in hearing other people's solutions!
This is my basic QGraphicsItem
.
class Node(QtWidgets.QGraphicsItem):
def __init__(self, scene, parent=None):
super(Node, self).__init__(parent)
scene.addItem(self)
# Variables
self.main_background_colour = QtGui.QColor(31, 176, 224)
self.title_background_colour = QtGui.QColor("#fffeb3")
self.name_background_colour = QtGui.QColor("#b8b64b")
self.brush = QtGui.QBrush(self.main_background_colour)
self.pen = QtGui.QPen(self.title_text_colour, 2)
self.main_rect = QtCore.QRectF(0, 0, 400, 200)
self.title_rect = QtCore.QRectF(self.main_rect.x() + (self.main_rect.width() * 0.05), self.main_rect.y() - 10, (self.main_rect.width() * 0.9), (self.main_rect.height() * 0.2))
self.name_rect = QtCore.QRectF(self.main_rect.x() + (self.main_rect.width() * 0.02), self.title_rect.bottom() - 10, (self.main_rect.width() * 0.96), (self.main_rect.height() * 0.3))
self.name_font_rect = QtCore.QRectF(self.name_rect.x() + (self.name_rect.width() * 0.05), self.name_rect.y() + 10, self.name_rect.width() * 0.9, self.name_rect.height() * 0.65)
# Flags
self.setFlag(self.ItemIsMovable, True)
self.setFlag(self.ItemSendsGeometryChanges, True)
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsFocusable, True)
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
def boundingRect(self):
return QtCore.QRectF(self.main_rect.x(), self.title_rect.y(), self.main_rect.width(), self.main_rect.height() + abs(self.main_rect.y() + self.title_rect.y()))
def paint(self, painter, option, widget=None):
# Border
if self.isSelected():
border_colour = QtGui.QColor(241, 175, 0)
else:
border_colour = self.main_background_colour.lighter()
self.pen.setColor(border_colour)
self.pen.setWidth(2)
painter.setPen(self.pen)
# Background
self.brush.setColor(self.main_background_colour)
painter.setBrush(self.brush)
painter.drawRoundedRect(self.main_rect, 4, 4)
# Name
self.brush.setColor(self.name_background_colour)
painter.setBrush(self.brush)
self.pen.setColor(QtGui.QColor("black"))
painter.setPen(self.pen)
painter.drawRoundedRect(self.name_rect, 4, 4)
# Tile
self.brush.setColor(self.title_background_colour)
painter.setBrush(self.brush)
self.pen.setColor(border_colour)
painter.setPen(self.pen)
painter.drawRoundedRect(self.title_rect, 4, 4)
UPDATE I have tried using the paint method as shown in musicamante answer, but it draws a border around all Rects individually, rather than the overall exterior shape.
The rest of my base code is the same, but I have changed the paint to match musicamante reply.
def paint(self, painter, option, widget=None):
path = QtGui.QPainterPath()
path.setFillRule(QtCore.Qt.WindingFill)
for rect in (self.main_rect, self.name_rect, self.title_rect):
path.addRoundedRect(rect, 4, 4)
if self.isSelected():
border_colour = QtGui.QColor(241, 175, 0)
else:
border_colour = self.main_background_colour.lighter()
painter.setPen(QtGui.QPen(border_colour, 2))
painter.drawPath(path)
To clarify, this is the border I am trying to achieve:
Solution
The simplest (but yet not optimal) solution is to create a QPainterPath based on all the rectangles, and draw the "final" border joining all the shapes the item contains:
def paint(self, painter, option, widget=None):
# draw all items here, using default values
# ...
path = QtGui.QPainterPath()
path.setFillRule(Qt.WindingFill)
for rect in (self.main_rect, self.name_rect, self.title_rect):
path.addRoundedRect(rect, 4, 4)
if self.isSelected():
border_colour = QtGui.QColor(241, 175, 0)
else:
border_colour = self.main_background_colour.lighter()
painter.setPen(QtGui.QPen(border_colour, 2))
painter.drawPath(path.simplified())
Be aware, though, that painting functions are called very often. Some level of caching and existing implementation is always preferred, especially considering that python is a huge bottleneck: you should always try to take advantage of the C++ implementation, possibly with existing base QGraphicsItem shapes.
For instance, instead of always painting three rounded rects from python, you could use a QGraphicsPathItem with a predefined QGraphicsPath set for each item.
Since you're already setting the ItemSendsGeometryChanges
flag, you could override itemChange()
to update that path whenever required, so that you don't need to create a new QPainterPath object for every paint()
call.
And if those items are used to show text, then use a QGraphicsPathItem with a QGraphicsSimpleTextItem set as a child of it.
Alternatively, consider using a QPicture as a class/instance variable that would be used as a "middle-cache" object: while it might add some level of complexity in the implementation, it certainly would improve performance, especially when multiple items are being shown.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.