Search code examples
pythonpysidepyside2qgraphicspathitem

QGraphicsPathItem issue with computing boundingRect


I am puzzled by a difference of behaviour between Qt5 (PySide2) and Qt4 (PySide). I get the impression that Qt5 has a bug but perhaps I do something wrong?

In short: when applying a computed QPainterPath to a QGraphicsPathItem (using setPath), the resulting size of the QGraphicsPathItem is bigger than the size of the QPainterPath itself by 1.5 pixels. This makes no sense to me, and with Qt4 the size was exactly the same.

I provide a simple piece of code to reproduce with both PySide and PySide2.

Using PySide:

#!/usr/bin/env python2

from PySide.QtCore import *
from PySide.QtGui import *

class Foo (QGraphicsPathItem):
    def __init__(self, parent):
        super(Foo, self).__init__()
        path = QPainterPath()
        path.addRect(0,0,10,10)
        print(str(path.boundingRect()))
        self.setPath(path)
        print(str(self.boundingRect()))

x=Foo(None)

And the result is:

$ python2 ./with_py2.py 
PySide.QtCore.QRectF(0.000000, 0.000000, 10.000000, 10.000000)
PySide.QtCore.QRectF(0.000000, 0.000000, 10.000000, 10.000000)

Same size, as expected. All good.

The exact same code with Qt5:

#!/usr/bin/env python3

from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *

class Foo (QGraphicsPathItem):
    def __init__(self, parent):
        super(Foo, self).__init__()
        path = QPainterPath()
        path.addRect(0,0,10,10)
        print(str(path.boundingRect()))
        self.setPath(path)
        print(str(self.boundingRect()))

x=Foo(None)

results in:

$ python3 bug.py 
PySide2.QtCore.QRectF(0.000000, 0.000000, 10.000000, 10.000000)
PySide2.QtCore.QRectF(-0.500000, -0.500000, 11.000000, 11.000000)

Does anyone see any obvious explanation?

Thanks


Solution

  • The boundingRect depend on the QPen of the QGraphicsPathItem to calculate it as seen in the source code.

    Qt4:

    QRectF QGraphicsPathItem::boundingRect() const
    {
        Q_D(const QGraphicsPathItem);
        if (d->boundingRect.isNull()) {
            qreal pw = pen().widthF();
            if (pw == 0.0)
                d->boundingRect = d->path.controlPointRect();
            else {
                d->boundingRect = shape().controlPointRect();
            }
        }
        return d->boundingRect;
    }
    

    Qt5

    QRectF QGraphicsPathItem::boundingRect() const
    {
        Q_D(const QGraphicsPathItem);
        if (d->boundingRect.isNull()) {
            qreal pw = pen().style() == Qt::NoPen ? qreal(0) : pen().widthF();
            if (pw == 0.0)
                d->boundingRect = d->path.controlPointRect();
            else {
                d->boundingRect = shape().controlPointRect();
            }
        }
        return d->boundingRect;
    }
    

    And if you check the Qt docs in both versions you will see that there was a change in the values of the QPen created by default:

    The default pen is a solid black brush with 0 width, square cap style (Qt::SquareCap), and bevel join style (Qt::BevelJoin).

    (emphasis mine)

    The default pen is a solid black brush with 1 width, square cap style (Qt::SquareCap), and bevel join style (Qt::BevelJoin).

    (emphasis mine)

    If you want to observe PySide behavior in PySide2 then set a QPen(Qt::NoPen) to the QGraphicsPathItem:

    class Foo(QGraphicsPathItem):
        def __init__(self, parent=None):
            super(Foo, self).__init__(parent)
            self.setPen(QPen(Qt.NoPen))
            path = QPainterPath()
            path.addRect(0, 0, 10, 10)
            print(str(path.boundingRect()))
            self.setPath(path)
            print(str(self.boundingRect()))
    
    
    x = Foo()
    

    Output

    PySide2.QtCore.QRectF(0.000000, 0.000000, 10.000000, 10.000000)
    PySide2.QtCore.QRectF(0.000000, 0.000000, 10.000000, 10.000000)