This actually seems like a bug, but it also requires some further inspecting to realize the difficulty of the problem.
First of all, the issue does not depend on the creation order: if you move the first polygon after any of the others, the problem remains.
The problem is related to the following aspects:
- the most important problem: those polygons are not closed;
- the first and last polygons, while similar and orthogonal (which makes things easier for collision detection), have a different order of vertices: while both rectangles are created starting with their top left vertex, the first goes clockwise (top left, top right, bottom right, bottom left), the last goes counterclockwise instead (top left, bottom left, bottom right, top right);
- QGraphicsItem uses the
shape()
function for collision detection (including mouse hover), which, in the case of QAbstractGraphicsShapeItem subclasses like QGraphicsPolygonItem, returns a complex QPainterPath created started with the item's "path" and adjusted using its pen()
, using a QGraphicsPathStroker, that creates a further QPainterPath that has the "contour" of the given path using the provided QPen;
Unfortunately, as powerful and smart QPainterPath is, it's not perfect: Qt developers had to balance between functionality and performance, since QPainterPaths are used a lot in Qt and therefore are required to be extremely light and fast.
The result is that the final shape()
of your paths is quite complex. Try to add the following override to your Polygon
class:
def paint(self, qp, opt, widget=None):
super().paint(qp, opt, widget)
qp.save()
qp.setPen(Qt.black)
qp.drawPath(self.shape())
qp.restore()
You'll see that the resulting paths of shape()
are quite more complex than expected:

Now, the problem raises because QGraphicsScene uses the QPainterPath contains()
function to detect whether the mouse cursor is within the graphics item boundaries. With such a complex path, the default ("fast") behavior potentially fails for very big outlines like those set by you.
I won't goo too much deep into mathematical aspects, but consider that collision detection is a well know dilemma and dealing with it means that some pros/cons decision has to be made when dealing with a generic API behavior. QPolygons might be convex or even have intersected lines: for instance, how would you deal with a polygon like this and its lines if they are too thick?

Now, assuming that your polygons always have consistent vertexes (meaning that there is absolutely no intersection, including those caused by the pen width), and you are not going to use too many items or shapes that are not that complex, there is a possible work around: provide the shape()
on your own.
The trick is to get the default returned shape()
, break it in toSubpathPolygons()
and iterate through them to check which of them does not contain any of the others.
Unfortunately, there is no certainty about what subpath polygon actually belongs to the boundary of the stroker: it theoretically is the first one, but it may not, so we need to carefully iterate through all of them using a while loop.
Note that, as explained above, the polygon must be closed. To simplify things, I've created a new QPolygonF in the __init__
(mutable objects should never be changed within the initialization of another object), but if you want to do things in the correct way you should add a further implementation that eventually returns a closed polygon and only if required.
class Polygon(QGraphicsPolygonItem):
def __init__(self, polygon):
if not polygon.isClosed():
polygon = QPolygonF(polygon)
polygon.append(polygon[0])
super().__init__(polygon)
self.setBrush(QBrush(QColor(255, 0, 0, 120)))
self.setPen(QPen(QColor(255, 0, 0), 10))
self.setAcceptHoverEvents(True)
def shape(self):
shape = super().shape().simplified()
polys = iter(shape.toSubpathPolygons(self.transform()))
outline = next(polys)
while True:
try:
other = next(polys)
except StopIteration:
break
for p in other:
# check if all points of the other polygon are *contained*
# within the current (possible) "outline"
if outline.containsPoint(p, Qt.WindingFill):
# the point is *inside*, meaning that the "other"
# polygon is probably an internal intersection
break
else:
# no intersection found, the "other" polygon is probably the
# *actual* outline of the QPainterPathStroker
outline = other
path = QPainterPath()
path.addPolygon(outline)
return path
Since the computation above may be a bit demanding for complex paths, you can consider to construct it only when required and eventually "cache" it as a private instance member, then clear it whenever any change might affect it (for polygon items, that would be the pen and polygon):
class Polygon(QGraphicsPolygonItem):
_cachedShape = None
def setPen(self, pen):
self._cachedShape = None
super().setPen(pen)
def setPolygon(self, polygon):
self._cachedShape = None
super().setPolygon(polygon)
def shape(self):
if not self._cachedShape:
self._cachedShape = self._createShape()
return self._cachedShape
def _createShape(self):
shape = super().shape().simplified()
polys = iter(shape.toSubpathPolygons(self.transform()))
outline = next(polys)
while True:
try:
other = next(polys)
except StopIteration:
break
for p in other:
# check if all points of the other polygon are *contained*
# within the current (possible) "outline"
if outline.containsPoint(p, Qt.WindingFill):
# the point is *inside*, meaning that the "other"
# polygon is probably an internal intersection
break
else:
# no intersection found, the "other" polygon is probably the
# *actual* outline of the QPainterPathStroker
outline = other
path = QPainterPath()
path.addPolygon(outline)
return path