Search code examples
pythonpyside2pyqtgraph

QGraphicsItem.itemClipsChildrenToShape issue in PySide2


I'm attempting to create a Smith Chart canvas using a combination of PySide2 and PyQtGraph. It seems the best way of doing this as described in this previous question (Pyqtgraph clip line) is to use QGraphicsEllipseItem to draw an outer circle, draw all the inner arcs using QGraphicsPathItem and QPainterPath, then add these to the outer circle and clip them using ItemClipsChildrenToParent.

Here is my code:

rline = [0.2, 0.5, 1.0, 2.0, 5.0]
xline = [0.2, 0.5, 1, 2, 5]

circle1 = QGraphicsEllipseItem(-1, -1, 2, 2)
circle1.setPen(mkPen('b', width=0.5))
circle1.setFlag(circle1.ItemClipsChildrenToShape)

pathItem = QGraphicsPathItem()
path = QPainterPath()
path.moveTo(1, 0)

for r in rline:
    raggio = 1./(1+r)
    path.addEllipse(1, -raggio, -raggio*2, raggio*2)

for x in xline:
    path.arcTo(1-x, 0, x*2, x*2, 90, 180)
    path.moveTo(1, 0)
    path.arcTo(1-x, 0, x*2, -x*2, 90, 180)

pathItem.setPath(path)
pathItem.setPen(mkPen('b', width = 0.5))
pathItem.setParentItem(circle1)

My code appears to be mostly working as shown by the image below.

enter image description here

The problem is that the inner arcs aren't quite bound by the outer circle. I would have thought that the clipping should be absolute so that the children cannot draw outside of the parents bounds but maybe this is not the case?


Solution

  • pyqtGraph by default creates cosmetic pens, which are pens that always have a width based on the device they're drawn: if you specify a cosmetic pen with a width equal to 0.5, it will always have that size no matter the scale used for the view.

    Now, the source of the problem can be found in the documentation about QGraphicsItem.shape(), which specifies:

    The outline of a shape can vary depending on the width and style of the pen used when drawing.

    Qt cannot know the actual extent of a cosmetic pen (since it completely depends on the paint device, and the same scene can have multiple views, each one with its own transformation. The result is that it computes the shape based on the specified pen width, even if it's cosmetic.

    Since you're using very small values, a 0.5 pen width actually becomes very big, and the extent of the shape is hugely augmented by that size, and that's why it seems that the children are not respecting the shape: if you have a 2x2 ellipse, with a pen of 0.5 the resulting shape will be a 2.5x2.5 ellipse (since the extent of the pen is half its width).

    The solution is to completely ignore the default behavior of shape(), and only return the shape of the ellipse using a subclass:

    class Ellipse(QGraphicsEllipseItem):
        def shape(self):
            path = QPainterPath()
            path.addEllipse(self.rect())
            return path
    
    circle1 = Ellipse(-1, -1, 2, 2)
    # ...