pyqt5parent-childtransformationqgraphicsitem

PyQt5: How to flip QGraphicsItem parent item only


I am trying to perform flipping on a QGraphicsItem that has child and grandchild QGraphicsItem.

The original item looks like this:

original

(The blue rectangle and text are child and the number inside it is grandchild

I apply the following transformation to the parent item:

parentItem.setTransformOriginPoint(parentItem.boundingRect().center())
parentItem.setTransform(QTransform.fromScale(-1, 1))

Result after flipping parent item:

parent_flipped

Since I want to reflip the text and number to be readable, I attempt to re-flip them after the parent's transformation as followed:

# For the child text
child.setTransformOriginPoint(child.boundingRect().center())
child.setTransform(QTransform.fromScale(-1, 1), True)
...

# For the grandchild number
grandchild.setTransformOriginPoint(grandchild.boundingRect().center())
grandchild.setTransform(QTransform.fromScale(-1, 1), True)

Here is the result after re-flipped the child and grandchild item:

child_grandchild_reflipped.

It seems that the translation is not correct. Can someone advice?

Thanks!

Minimal reproducible example below:

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class ParentItem(QGraphicsRectItem):
    def __init__(self, pos, name, parent=None):
        w, h = 550, 220
        super().__init__(-w/2, -h/2, w, h)
        self.setPos(pos)

        self.name = ChildText(self.boundingRect().topLeft() - QPointF(0, 100), f">NAME{name}", self)
        self.value = ChildText(self.boundingRect().topLeft()- QPointF(0, 50), f">VALUE_{name}", self)

        self.ChildPad1 = ChildPad(QPointF(-150, 0), "1", self)
        self.ChildPad2 = ChildPad(QPointF(+150, 0), "2", self)

        self.color = QColor(192, 192, 192)
        self.setPen(QPen(self.color, 5))

        self.setFlag(self.ItemIsMovable, True)
    def flipParent(self):
        self.setTransformOriginPoint(self.boundingRect().center())
        self.setTransform(QTransform.fromScale(-1, 1))
    
    def reflipChilds(self):
        # Child Texts
        self.name.setTransformOriginPoint(self.name.boundingRect().center())
        self.name.setTransform(QTransform.fromScale(-1, 1))

        self.value.setTransformOriginPoint(self.value.boundingRect().center())
        self.value.setTransform(QTransform.fromScale(-1, 1))

        # GrandChild Numbers
        for child in self.childItems():
            if isinstance(child, ChildPad):
                child.Number.setTransformOriginPoint(child.Number.boundingRect().center())
                child.Number.setTransform(QTransform.fromScale(-1, 1))

class ChildText(QGraphicsTextItem):
    def __init__(self, pos, text=">Text", parent=None): 
        super().__init__(parent) 
        self.setPos(pos)

        self.parent = parent
        self.text = text
        self.color = QColor(255, 0, 0)
        
        self.setDefaultTextColor(self.color)      
        self.setFlag(self.ItemIsMovable, True)
        
        f = QFont()
        f.setPointSizeF(min(self.parent.boundingRect().width()/8, self.parent.boundingRect().height()/8))
        self.setFont(f)
        self.setHtml(f"<p><center>{self.text}</center></p>")

class ChildPad(QGraphicsRectItem):
    def __init__(self, pos, pinNumber, parent=None):
        w, h = 200, 100 
        super().__init__(-w/2, -h/2, w, h, parent)  
        self.setPos(pos)

        self.parent = parent
        self.color = QColor(255, 0, 0)

        self.setPen(QPen(self.color, Qt.MiterJoin, 1))
        self.setBrush(QBrush(self.color))

        self.Number = GrandChildNumber(pinNumber, self)

class GrandChildNumber(QGraphicsTextItem):
    def __init__(self, pinNumber, parent=None):
        super().__init__(parent)
        self.parent = parent

        self.color = QColor(32, 32, 32)

        self.setHtml(f"{pinNumber}")
        self.moveToParentCenter()
    
    def moveToParentCenter(self):
            f = QFont()
            f.setPointSizeF(min(self.parent.boundingRect().width()/4, self.parent.boundingRect().height()/4))    
            self.setFont(f)
            rect = self.boundingRect()
            rect.moveCenter(self.parent.boundingRect().center())
            self.setPos(rect.topLeft())
            self.adjustSize()

def main():
    import sys
    app = QtWidgets.QApplication(sys.argv)
    scene = QGraphicsScene()

    # No Transformation applied
    originalItem = ParentItem(QPointF(300, 100), "ORIGINAL", scene)
    scene.addItem(originalItem)

    # Flipped the whole parent item
    flipParentItem = ParentItem(QPointF(300, 500), "FLIPP_PARENT", scene)
    flipParentItem.flipParent()
    scene.addItem(flipParentItem)

    # Flipped the whole parent item, then reflip the Text and Number
    reflipChildItem = ParentItem(QPointF(300, 900), "REFLIP_CHILDS", scene)
    flipParentItem.flipParent()
    reflipChildItem.reflipChilds()
    scene.addItem(reflipChildItem) 

    view = QtWidgets.QGraphicsView(scene)
    view.setRenderHints(QtGui.QPainter.Antialiasing)
    view.show()
    sys.exit(app.exec_())
    
if __name__ == '__main__':
    main()


Solution

  • If you want to ignore transformations, you should use the ItemIgnoresTransformations flag.

    Then, be aware that setTransformation() doesn't apply the transformation "on top" of the existing one, but completely sets a new transformation on the item (transforms inherited from the parent are not considered).

    A proper flip() function should toggle the transformation, so it must consider the current transform() and flip it.

    Now, the problem with text items is that they always use the origin point as the top left of their contents. While the basic repositioning might work for generic usage, it will not whenever any parent has a transformation.

    The problem you're seeing is because you are just mapping the position based on the parent, but since the parent is "flipped", the top left corner of the resulting rect is on the opposite side (relative to the center) in parent coordinates.

    To properly get the actual position relative to the parent, you must always use the cumulative transformations, which means map coordinates to the scene and map them back to the parent.

    In order to make the code simpler to understand, I moved the repositioning function to the parent of the text item.

    class ParentItem(QGraphicsRectItem):
        # ...
        def flip(self):
            self.setTransformOriginPoint(self.boundingRect().center())
            self.setTransform(self.transform().scale(-1, 1))
    
            for child in self.childItems():
                if isinstance(child, ChildPad):
                    child.updateNumber()
    
    
    class ChildPad(QGraphicsRectItem):
        def __init__(self, pos, pinNumber, parent=None):
            w, h = 200, 100 
            super().__init__(-w/2, -h/2, w, h, parent)  
            self.setPos(pos)
    
            self.parent = parent
            self.color = QColor(255, 0, 0)
    
            self.setPen(QPen(self.color, Qt.MiterJoin, 1))
            self.setBrush(QBrush(self.color))
    
            self.number = GrandChildNumber(pinNumber, self)
            self.updateNumber()
    
        def updateNumber(self):
            br = self.boundingRect()
            f = QFont()
            f.setPointSizeF(min(br.width() / 4, br.height() / 4))    
            self.number.setFont(f)
    
            # get the "visual" rect of the parent in scene coordinates
            parentRect = self.mapToScene(br).boundingRect()
            rect = self.number.boundingRect()
            rect.moveCenter(parentRect.center())
            # map the new rect position *from* the scene in local coordinates
            topLeft = self.mapFromScene(rect.topLeft())
            self.number.setPos(topLeft)
    
    
    class GrandChildNumber(QGraphicsTextItem):
        def __init__(self, pinNumber, parent=None):
            super().__init__(parent)
            self.parent = parent
            self.setFlag(self.ItemIgnoresTransformations)
    
            self.color = QColor(32, 32, 32)
    
            self.setHtml(str(pinNumber))