Issue
I am overwriting an item that can be either rectangle or circle (share the same functions, properties). I'd like to have one class that is inherited from both shape as followed:
class Pad(QGraphicsRectItem, QGraphicsEllipseItem):
def __init__(self, pos, isCircle=False, parent=None):
w, h = 200, 100
if isCircle:
QGraphicsEllipseItem.__init__(self, -w/2, -h/2, w, h)
else:
QGraphicsRectItem.__init__(self, -w/2, -h/2, w, h)
This results with the only shape of the first parent (QGraphicsRectItem
in this case). Could someone explain why and provide suggestion?
Full code below:
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Pad(QGraphicsRectItem, QGraphicsEllipseItem):
def __init__(self, pos, isCircle=False, parent=None):
w, h = 200, 100
if isCircle:
QGraphicsEllipseItem.__init__(self, -w/2, -h/2, w, h)
else:
QGraphicsRectItem.__init__(self, -w/2, -h/2, w, h)
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))
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QGraphicsScene()
rect = Pad(QPointF(0, 0), False, scene)
circle = Pad(QPointF(300, 300), True, scene)
scene.addItem(rect)
scene.addItem(circle)
view = QtWidgets.QGraphicsView(scene)
view.setRenderHints(QtGui.QPainter.Antialiasing)
view.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Solution
TL;DR
Use a QGraphicsPathItem.
Explanation
The order of the base classes decides the final behavior of all methods: it's called method resolution order (MRO). The fact that you called the __init__
of a different class is completely irrelevant.
Consider the following:
class Rectangle:
def paint(self):
print('draw rectangle')
class Circle:
def paint(self):
print('draw circle')
class Test1(Rectangle, Circle):
def __init__(self):
Rectangle.__init__(self)
class Test2(Circle, Rectangle):
def __init__(self):
# note that we're still calling the __init__ of the Rectangle class
Rectangle.__init__(self)
>>> Test1().paint()
'draw rectangle'
>>> Test2().paint()
'draw circle'
This is because any override of any inherited base class will take precedence, based on the [reverse] order of inheritance: the method that will be actually called will be the first override in the MRO.
While, under certain aspects, the QGraphicsRectItem and QGraphicsEllipseItem classes are very similar, you must also always remember that PyQt (and PySide) are bindings: their behavior not only depends on the python class, but also on their wrapped C++ objects.
That's why it's usually impossible to use multiple inheritance of C++ derived classes[1].
If those classes were pure python, the solution would be quite easy: considering what explained above, the paint method should also call the method of the correct class:
class Pad(QGraphicsRectItem, QGraphicsEllipseItem):
def __init__(self, pos, isCircle=False, parent=None):
# ...
self.isCircle = isCircle
def paint(self, painter, option, widget=None):
if self.isCircle:
QGraphicsEllipseItem.paint(self, painter, option, widget)
else:
QGraphicsRectItem.paint(self, painter, option, widget)
Unfortunately, this is not the case for wrapped methods of bound C++ objects, as they internally access memory addresses for their properties, and you will most certainly get some unexpected behavior. If you're lucky, you'll only have strange graphical results. Worst case scenarios include the program freezing or even crash.
For instance, try adding the following line in the if isCircle:
block of the init:
print(self.startAngle(), self.spanAngle())
Running the above gives unexpected (and unpredictable) results, like 97 -1220601744
or 105 -1220401040
. Adding the same line in paint
also gives a completely different result than what was printed in the init.
The solution is quite simple: don't try to use multiple inheritance, but use a better class as QGraphicsPathItem:
class Pad(QGraphicsPathItem):
def __init__(self, pos, isCircle=False, parent=None):
w, h = 200, 100
rect = QRectF(-w/2, -h/2, w, h)
path = QPainterPath()
if isCircle:
path.addEllipse(rect)
else:
path.addRect(rect)
super().__init__(path, 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))
Note: the parent
argument of a QGraphicsItem must always be a QGraphicsItem based class; if you intend to directly provide the scene for some reason, use a specific positional or keyworded argument (in your code you didn't use the parent
argument in the class, but you provided it anyway, and it was of the wrong type); also consider that if you're trying to do that so that the item adds itself to the scene, that's generally not considered good practice, unless you really know what you're doing.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.