'Draw a border around all objects/shapes in a QGraphicsItem paint method
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)
Solution 1:[1]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |


