I'm writing an application that allows the user to add items to a scene. I dont want any new items being drawn over items that have already been drawn and to do that I decided to use the collidesWithItem() function to detect collision. With my code i still can draw over added items even though there is obviously collision and I debugged the program and the collidesWithItem() function keeps returning "False".
The items are added by clicking on the toolbar of the Form.
Down below is my code:
class graphicsScene(QtGui.QGraphicsScene, QtGui.QWidget):
def __init__(self, parent=None):
super(graphicsScene, self).__init__(parent)
self.i = 0
self.setSceneRect(-180, -90, 360, 180)
global overlapped
overlapped = 0
def mousePressEvent(self, event):
global host_cs
global overlapped
if host_cs == 1:
if len(hostItem_list) == 0:
self.host_item = host_Object()
hostItem_list.append(self.host_item.host_pixItem)
else:
self.host_item = host_Object()
for host in hostItem_list:
if self.host_item.host_pixItem.collidesWithItem(host):
print 'collision'
overlapped = 1
break
elif self.host_item.host_pixItem.collidesWithItem(host) == False:
overlapped = 0
if overlapped == 0:
hostItem_list.append(self.host_item.host_pixItem)
def mouseReleaseEvent(self, event):
global host_cs
if host_cs == 1:
if overlapped == 0:
self.addItem(self.host_item.host_pixItem)
self.host_item.host_pixItem.setPos(event.scenePos())
self.i += 1
host_list.append('h' + str(self.i))
class host_Object(QtGui.QGraphicsPixmapItem, QtGui.QWidget):
def __init__(self, parent=None):
super(host_Object, self).__init__(parent)
pixmap = QtGui.QPixmap("host.png")
self.host_pixItem = QtGui.QGraphicsPixmapItem(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio))
self.host_pixItem.setFlag(QtGui.QGraphicsPixmapItem.ItemIsSelectable)
self.host_pixItem.setFlag(QtGui.QGraphicsPixmapItem.ItemIsMovable)
class Form(QtGui.QMainWindow, QtGui.QWidget):
def __init__(self):
super(Form, self).__init__()
self.ui = uic.loadUi('form.ui')
self.ui.actionHost.triggered.connect(self.place_host)
self.scene = graphicsScene()
self.ui.view.setScene(self.scene)
def place_host(self):
host_cs = 1
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
form = Form()
form.ui.show()
app.exec_()
Multiple inheritance is source of all evil things in application design.
Also it is forbidden in Qt to double inherit QObject.
So all your classes with multiple inheritance has big flaws.
Even class host_Object(QtGui.QGraphicsPixmapItem, QtGui.QWidget) is wrong and problematic since item can't be a QWidget and QGraphicsItem at the same time.
I would recommend you to avoid double inheritance as a general rule not only for Qt framework but for any language.
Related
I have some custom items in the scene. I would like to allow the user to connect the two items using the mouse. I checked an answer in this question but there wasn't a provision to let users connect the two points. (Also, note that item must be movable)
Here is a demonstration of how I want it to be:
I want the connection between the two ellipses as shown above
Can I know how this can be done?
While the solution proposed by JacksonPro is fine, I'd like to provide a slightly different concept that adds some benefits:
improved object structure and control;
more reliable collision detection;
painting is slightly simplified by making it more object-compliant;
better readability (mostly by using less variables and functions);
clearer connection creation (the line "snaps" to control points);
possibility to have control points on both sides (also preventing connections on the same side) and to remove a connection if already exists (by "connecting" again the same points);
connections between multiple control points;
it's not longer ;-)
The idea is to have control points that are actual QGraphicsItem objects (QGraphicsEllipseItem) and children of CustomItem.
This not only simplifies painting, but also improves object collision detection and management: there is no need for a complex function to draw the new line, and creating an ellipse that is drawn around its pos ensures that we already know the targets of the line by getting their scenePos(); this also makes it much more easy to detect if the mouse cursor is actually inside a control point or not.
Note that for simplification reasons I set some properties as class members. If you want to create subclasses of the item for more advanced or customized controls, those parameters should be created as instance attributes; in that case, you might prefer to inherit from QGraphicsRectItem: even if you'll still need to override the painting in order to draw a rounded rect, it will make it easier to set its properties (pen, brush and rectangle) and even change them during runtime, so that you only need to access those properties within paint(), while also ensuring that updates are correctly called when Qt requires it.
from PyQt5 import QtCore, QtGui, QtWidgets
class Connection(QtWidgets.QGraphicsLineItem):
def __init__(self, start, p2):
super().__init__()
self.start = start
self.end = None
self._line = QtCore.QLineF(start.scenePos(), p2)
self.setLine(self._line)
def controlPoints(self):
return self.start, self.end
def setP2(self, p2):
self._line.setP2(p2)
self.setLine(self._line)
def setStart(self, start):
self.start = start
self.updateLine()
def setEnd(self, end):
self.end = end
self.updateLine(end)
def updateLine(self, source):
if source == self.start:
self._line.setP1(source.scenePos())
else:
self._line.setP2(source.scenePos())
self.setLine(self._line)
class ControlPoint(QtWidgets.QGraphicsEllipseItem):
def __init__(self, parent, onLeft):
super().__init__(-5, -5, 10, 10, parent)
self.onLeft = onLeft
self.lines = []
# this flag **must** be set after creating self.lines!
self.setFlags(self.ItemSendsScenePositionChanges)
def addLine(self, lineItem):
for existing in self.lines:
if existing.controlPoints() == lineItem.controlPoints():
# another line with the same control points already exists
return False
self.lines.append(lineItem)
return True
def removeLine(self, lineItem):
for existing in self.lines:
if existing.controlPoints() == lineItem.controlPoints():
self.scene().removeItem(existing)
self.lines.remove(existing)
return True
return False
def itemChange(self, change, value):
for line in self.lines:
line.updateLine(self)
return super().itemChange(change, value)
class CustomItem(QtWidgets.QGraphicsItem):
pen = QtGui.QPen(QtCore.Qt.red, 2)
brush = QtGui.QBrush(QtGui.QColor(31, 176, 224))
controlBrush = QtGui.QBrush(QtGui.QColor(214, 13, 36))
rect = QtCore.QRectF(0, 0, 100, 100)
def __init__(self, left=False, right=False, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFlags(self.ItemIsMovable)
self.controls = []
for onLeft, create in enumerate((right, left)):
if create:
control = ControlPoint(self, onLeft)
self.controls.append(control)
control.setPen(self.pen)
control.setBrush(self.controlBrush)
if onLeft:
control.setX(100)
control.setY(35)
def boundingRect(self):
adjust = self.pen.width() / 2
return self.rect.adjusted(-adjust, -adjust, adjust, adjust)
def paint(self, painter, option, widget=None):
painter.save()
painter.setPen(self.pen)
painter.setBrush(self.brush)
painter.drawRoundedRect(self.rect, 4, 4)
painter.restore()
class Scene(QtWidgets.QGraphicsScene):
startItem = newConnection = None
def controlPointAt(self, pos):
mask = QtGui.QPainterPath()
mask.setFillRule(QtCore.Qt.WindingFill)
for item in self.items(pos):
if mask.contains(pos):
# ignore objects hidden by others
return
if isinstance(item, ControlPoint):
return item
if not isinstance(item, Connection):
mask.addPath(item.shape().translated(item.scenePos()))
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
item = self.controlPointAt(event.scenePos())
if item:
self.startItem = item
self.newConnection = Connection(item, event.scenePos())
self.addItem(self.newConnection)
return
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.newConnection:
item = self.controlPointAt(event.scenePos())
if (item and item != self.startItem and
self.startItem.onLeft != item.onLeft):
p2 = item.scenePos()
else:
p2 = event.scenePos()
self.newConnection.setP2(p2)
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.newConnection:
item = self.controlPointAt(event.scenePos())
if item and item != self.startItem:
self.newConnection.setEnd(item)
if self.startItem.addLine(self.newConnection):
item.addLine(self.newConnection)
else:
# delete the connection if it exists; remove the following
# line if this feature is not required
self.startItem.removeLine(self.newConnection)
self.removeItem(self.newConnection)
else:
self.removeItem(self.newConnection)
self.startItem = self.newConnection = None
super().mouseReleaseEvent(event)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
scene = Scene()
scene.addItem(CustomItem(left=True))
scene.addItem(CustomItem(left=True))
scene.addItem(CustomItem(right=True))
scene.addItem(CustomItem(right=True))
view = QtWidgets.QGraphicsView(scene)
view.setRenderHints(QtGui.QPainter.Antialiasing)
view.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
A small suggestion: I've seen that you have the habit of always creating objects in the paint method, even if those values are normally "hardcoded"; one of the most important aspects of the Graphics View framework is its performance, which can be obviously partially degraded by python, so if you have properties that are constant during runtime (rectangles, pens, brushes) it's usually better to make them more "static", at least as instance attributes, in order to simplify the painting as much as possible.
For this, you might have to implement your own scene class by inheriting QGraphicsScene and overriding the mouse events.
Here is the code which you may improve:
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
class CustomItem(QtWidgets.QGraphicsItem):
def __init__(self, pointONLeft=False, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ellipseOnLeft = pointONLeft
self.point = None
self.endPoint =None
self.isStart = None
self.line = None
self.setAcceptHoverEvents(True)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemSendsGeometryChanges)
def addLine(self, line, ispoint):
if not self.line:
self.line = line
self.isStart = ispoint
def itemChange(self, change, value):
if change == self.ItemPositionChange and self.scene():
self.moveLineToCenter(value)
return super(CustomItem, self).itemChange(change, value)
def moveLineToCenter(self, newPos): # moves line to center of the ellipse
if self.line:
if self.ellipseOnLeft:
xOffset = QtCore.QRectF(-5, 30, 10, 10).x() + 5
yOffset = QtCore.QRectF(-5, 30, 10, 10).y() + 5
else:
xOffset = QtCore.QRectF(95, 30, 10, 10).x() + 5
yOffset = QtCore.QRectF(95, 30, 10, 10).y() + 5
newCenterPos = QtCore.QPointF(newPos.x() + xOffset, newPos.y() + yOffset)
p1 = newCenterPos if self.isStart else self.line.line().p1()
p2 = self.line.line().p2() if self.isStart else newCenterPos
self.line.setLine(QtCore.QLineF(p1, p2))
def containsPoint(self, pos): # checks whether the mouse is inside the ellipse
x = self.mapToScene(QtCore.QRectF(-5, 30, 10, 10).adjusted(-0.5, 0.5, 0.5, 0.5)).containsPoint(pos, QtCore.Qt.OddEvenFill) or \
self.mapToScene(QtCore.QRectF(95, 30, 10, 10).adjusted(0.5, 0.5, 0.5, 0.5)).containsPoint(pos,
QtCore.Qt.OddEvenFill)
return x
def boundingRect(self):
return QtCore.QRectF(-5, 0, 110, 110)
def paint(self, painter, option, widget):
pen = QtGui.QPen(QtCore.Qt.red)
pen.setWidth(2)
painter.setPen(pen)
painter.setBrush(QtGui.QBrush(QtGui.QColor(31, 176, 224)))
painter.drawRoundedRect(QtCore.QRectF(0, 0, 100, 100), 4, 4)
painter.setBrush(QtGui.QBrush(QtGui.QColor(214, 13, 36)))
if self.ellipseOnLeft: # draws ellipse on left
painter.drawEllipse(QtCore.QRectF(-5, 30, 10, 10))
else: # draws ellipse on right
painter.drawEllipse(QtCore.QRectF(95, 30, 10, 10))
# ------------------------Scene Class ----------------------------------- #
class Scene(QtWidgets.QGraphicsScene):
def __init__(self):
super(Scene, self).__init__()
self.startPoint = None
self.endPoint = None
self.line = None
self.graphics_line = None
self.item1 = None
self.item2 = None
def mousePressEvent(self, event):
self.line = None
self.graphics_line = None
self.item1 = None
self.item2 = None
self.startPoint = None
self.endPoint = None
if self.itemAt(event.scenePos(), QtGui.QTransform()) and isinstance(self.itemAt(event.scenePos(),
QtGui.QTransform()), CustomItem):
self.item1 = self.itemAt(event.scenePos(), QtGui.QTransform())
self.checkPoint1(event.scenePos())
if self.startPoint:
self.line = QtCore.QLineF(self.startPoint, self.endPoint)
self.graphics_line = self.addLine(self.line)
self.update_path()
super(Scene, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton and self.startPoint:
self.endPoint = event.scenePos()
self.update_path()
super(Scene, self).mouseMoveEvent(event)
def filterCollidingItems(self, items): # filters out all the colliding items and returns only instances of CustomItem
return [x for x in items if isinstance(x, CustomItem) and x != self.item1]
def mouseReleaseEvent(self, event):
if self.graphics_line:
self.checkPoint2(event.scenePos())
self.update_path()
if self.item2 and not self.item1.line and not self.item2.line:
self.item1.addLine(self.graphics_line, True)
self.item2.addLine(self.graphics_line, False)
else:
if self.graphics_line:
self.removeItem(self.graphics_line)
super(Scene, self).mouseReleaseEvent(event)
def checkPoint1(self, pos):
if self.item1.containsPoint(pos):
self.item1.setFlag(self.item1.ItemIsMovable, False)
self.startPoint = self.endPoint = pos
else:
self.item1.setFlag(self.item1.ItemIsMovable, True)
def checkPoint2(self, pos):
item_lst = self.filterCollidingItems(self.graphics_line.collidingItems())
contains = False
if not item_lst: # checks if there are any items in the list
return
for self.item2 in item_lst:
if self.item2.containsPoint(pos):
contains = True
self.endPoint = pos
break
if not contains:
self.item2 = None
def update_path(self):
if self.startPoint and self.endPoint:
self.line.setP2(self.endPoint)
self.graphics_line.setLine(self.line)
def main():
app = QtWidgets.QApplication(sys.argv)
scene = Scene()
item1 = CustomItem(True)
scene.addItem(item1)
item2 = CustomItem()
scene.addItem(item2)
view = QtWidgets.QGraphicsView(scene)
view.setViewportUpdateMode(view.FullViewportUpdate)
view.setMouseTracking(True)
view.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Explanation of the above code:
I make my own custom Item by inheriting the QGraphicsItem. pointONLeft=False is to check which side the ellipse is to be drawn. If pointONLeft=True, then red circle that you see in the question's image will be drawn on the left.
The addLine, itemChange and moveLineToCenter methods are taken from here. I suggest you go through that answer before moving on.
The containsPoint method inside the CustomItem checks whether the mouse is inside the circle. This method will be accessed from the custom Scene, if the mouse is inside the circle it will disable the movement by using CustomiItem.setFlag(CustomItem.ItemIsMovable, False).
To draw the line I use the QLineF provided by PyQt. If you want to know how to draw a straight line by dragging I suggest you refer this. while the explanation is for qpainterpath same can be applied here.
The collidingItems() is a method provided by QGraphicsItem. It returns all the items that are colliding including the line itself. So, I created the filterCollidingItems to filter out only the items that are instances of CustomItem.
(Also, note that collidingItems() returns the colliding items in the reverse order they are inserted i,e if CustomItem1 is inserted first and CustomItem second then if the line collides the second item will be returned first. So if two items are on each other and the line is colliding then the last inserted item will become item2 you can change this by changing the z value)
Readers can add suggestions or queries in the comments. If you have a better answer, feel free to write.
I'm writing a program that should create a QGraphicsItem when a double-click happens (inside the scene area), and also that item must be created at double-click position. I've already written some code, but it doesn't work properly. When I double-click on the scene, an item gets created, but at a completely different place. So right now that is the problem. I need it to be created at mouse's position.
Here is my code:
ui_path = "C:/Users/User/Desktop/programming/Creator/Creator.ui"
class Creator(QtWidgets.QWidget):
count = 0
def __init__(self):
super(Creator, self).__init__()
loader = QtUiTools.QUiLoader()
self.ui = loader.load(ui_path, self)
self.variables()
self.ui.canvas_area.viewport().installEventFilter(self) #canvas_area is the QGraphicsView
self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.CustomizeWindowHint | Qt.WindowStaysOnTopHint)
def variables(self):
self.scene = QtWidgets.QGraphicsScene()
self.ui.canvas_area.setScene(self.scene)
def eventFilter(self, obj, event):
if obj is self.ui.canvas_area.viewport():
if event.type() == QtCore.QEvent.MouseButtonDblClick:
self.createItems(event)
return super(Creator, self).eventFilter(obj, event)
def createItems(self, event):
pos = event.pos()
self._x = pos.x()
self._y = pos.y()
rect = self.scene.addRect(self._x, self._y, 40, 40, QPen(Qt.red), QBrush(Qt.gray))
rect.setFlag(QGraphicsItem.ItemIsMovable)
rect.setFlag(QGraphicsItem.ItemIsFocusable)
rect.setFlag(QGraphicsItem.ItemIsSelectable)
if __name__ == '__main__':
creator_window = Creator()
creator_window.ui.show()
I've read something about mapFromScene and mapToScene, and that these can solve the problem - but I don't really understand how to use them. Also there are some examples of this, but all that I've found were in C++, which I know nothing about. So if someone could help me figure out how to solve this problem I would really appreciate that.
You first must set the scene-rect for the scene:
self.scene.setSceneRect(0, 0, 1000, 1000)
then you must convert the event position to scene co-ordinates, like this:
pos = self.canvas_area.mapToScene(event.pos())
and that should be all that's needed to fix your example.
A different approach that may be worth considering would be to install an event-filter on the scene instead:
self.scene.installEventFilter(self)
and then filter on the more specialised graphics events:
def eventFilter(self, obj, event):
if obj is self.scene:
if event.type() == QtCore.QEvent.GraphicsSceneMouseDoubleClick:
self.createItems(event)
return super(Creator, self).eventFilter(obj, event)
This creates a QGraphicsSceneMouseEvent, which has several useful features that a standard QMouseEvent doesn't have. This includes scenePos(), which saves having to map to scene co-ordinates all the time, allowing you to then simply do:
pos = event.scenePos()
I am trying to animate a QGraphicsPixmapItem in PyQt5.
The program as is crashes without any error message, and removing the lines about the variable 'anime' makes the program work normally.
here is the QGraphicsPixMapItem:
class QStone(QGraphicsPixmapItem,QGraphicsObject):
def __init__(self, color, movable):
QGraphicsPixmapItem.__init__(self)
QGraphicsObject.__init__(self)
if movable:
self.setFlag(QGraphicsItem.ItemIsMovable)
white = QPixmap("ressources/white2.png")
black = QPixmap("ressources/black_stone.png")
empty = QPixmap("ressources/no_stone.jpg")
if color == Player.white:
self.setPixmap(white.scaled(60, 60, Qt.KeepAspectRatio))
elif color == Player.black:
self.setPixmap(black.scaled(60, 60, Qt.KeepAspectRatio))
self.w = self.boundingRect().width()
self.h = self.boundingRect().height()
def hoverEnterEvent(self, event):
self.setCursor(Qt.OpenHandCursor)
self.setOpacity(0.5)
event.accept()
def hoverLeaveEvent(self, event):
self.setCursor(Qt.ArrowCursor)
self.setOpacity(1.)
event.accept()
the QGraphicsObject inheritance seems to be required for using QPropertyAnimation.
here is the code containing this animation:(this method belongs to a QGraphicsView's subclass):
def display_stone(self, x, y, color=None):
stone = ""
# if color is None:
# stone = QStone("", True)
if color == Player.white:
stone = QStone(Player.white, False)
elif color == Player.black:
stone = QStone(Player.black, False)
stone.setOpacity(0.0)
anime = QPropertyAnimation(stone, b"opacity",self)
anime.setDuration(800)
anime.setStartValue(0.0)
anime.setEndValue(1.0)
anime.start()
stone.setPos(x - stone.w / 2, y - stone.h / 2)
self.scene.addItem(stone)
stone.setZValue(10)
any idea?
thank you
Unlike the Qt C++ API, PyQt does not allow double inheritance (except in exceptional cases(1)) so you cannot implement a class that inherits from QGraphicsPixmapItem and QGraphicsObject.
In this case there are the following options:
1. In this case it is to create a QObject that handles the property you want to modify, and that is the object that is handled by the QPropertyAnimation:
class OpacityManager(QObject):
opacityChanged = pyqtSignal(float)
def __init__(self, initial_opacity, parent=None):
super(OpacityManager, self).__init__(parent)
self._opacity = initial_opacity
#pyqtProperty(float, notify=opacityChanged)
def opacity(self):
return self._opacity
#opacity.setter
def opacity(self, v):
if self._opacity != v:
self._opacity = v
self.opacityChanged.emit(self._opacity)
class QStone(QGraphicsPixmapItem):
def __init__(self, color, movable=False):
QGraphicsPixmapItem.__init__(self)
self.manager = OpacityManager(self.opacity())
self.manager.opacityChanged.connect(self.setOpacity)
if movable:
self.setFlag(QGraphicsItem.ItemIsMovable)
# ...
# ...
anime = QPropertyAnimation(stone.manager, b"opacity", stone.manager)
# ...
2. Another option is QVariantAnimation:
# ...
anime = QVariantAnimation(self)
anime.valueChanged.connect(stone.setOpacity)
anime.setDuration(800)
# ...
(1) https://www.riverbankcomputing.com/static/Docs/PyQt5/qt_interfaces.html
I have an application that works kind of like a slide show: I have a button that changes the background picture when clicked. I also have a second button that helps go back to the previous picture. The problem is, that the first button gets another function at the end of the show, but after that I am not able to change the function back to the previous one when I click the back button.
My code looks somewhat like this, I hope this makes my problem clear:
class SecondWindow(TemplateBaseClass):
def back(self):
self.first.n = self.first.n-2
self.hide()
self.first.show()
self.first.nextPicture()
def __init__(self):
TemplateBaseClass.__init__(self)
self.ui = WindowTemplate()
self.ui.setupUi(self)
self.first = MainWindow(self)
self.first.showFullScreen()
self.ui.pushButton.clicked.connect(lambda x:self.back())
class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
def showSecond(self):
#QTimer.singleShot(25, MainWindow)
self.second.showFullScreen()
self.hide()
def back(self):
if self.n >= 2:
self.n = self.n-2
self.notBack = False
self.nextPicture()
# I tried in several places like here, but it does not work
self.ui.end_button.clicked.connect(lambda x:self.nextPicture())
def nextPicture(self):
print(self.n)
if self.n == 0:
self.ui.bg_widget.setStyleSheet("background-image: url(:/ux/img0.png);\nbackground-repeat:no-repeat;")
elif self.n ==1 :
self.ui.bg_widget.setStyleSheet("background-image: url(:/ux/img1.png);\nbackground-repeat:no-repeat;")
elif self.n == 2:
self.ui.bg_widget.setStyleSheet("background-image: url(:/ux/img2.png);\nbackground-repeat:no-repeat;")
if self.notBack:
self.ui.end_button.clicked.connect(lambda x:self.showSecond())
else:
self.ui.end_button.clicked.connect(lambda x:self.nextPicture())
self.n +=1
self.notBack = True
def __init__(self, second):
QtGui.QMainWindow.__init__(self)
Ui_MainWindow.__init__(self)
self.second = second
self.setWindowTitle('pyqtgraph example: Qt Designer')
self.ui=uic.loadUi(uiFile, self)
self.setupUi(self)
self.setWindowFlags(QtCore.Qt.CustomizeWindowHint)
self.showFullScreen()
self.n = 1
self.notBack = True
self.ui.end_button.clicked.connect(lambda x:self.nextPicture())
self.ui.backButton.clicked.connect(lambda x:self.back())
A simple disconnect() solves the issue:
def back(self):
if self.n >= 2:
self.n = self.n-2
self.notBack = False
self.nextPicture()
self.ui.end_button.disconnect()
self.ui.end_button.clicked.connect(lambda x:self.nextPicture())
Me and my colleagues are writing a data processing application in python.
We are currently working on the frontend part of the application.
We have a big problem though, that's that the application gets the following error after a random amount of time:
QWidget::repaint: Recursive repaint detected
This one also pops up from time to time:
QPainter::begin: Paint device returned engine == 0, type: 1
This is the file where all gui related stuff happens, I cut out the irrelevant methods for the sake of not being to lengthy:
gfx.py:
import sys, random, math
from PyQt4 import QtGui, QtCore
from random import randrange
from eventbased import listener
app = QtGui.QApplication(sys.argv)
def exec():
return app.exec_()
class MapView(QtGui.QMainWindow, listener.Listener):
def __init__(self, mapimagepath = 0, nodes = 0):
QtGui.QMainWindow.__init__(self)
listener.Listener.__init__(self)
self.setWindowTitle('Population mapping')
self.map = Map(self, mapimagepath)
self.setCentralWidget(self.map)
self.map.start()
self.center()
def center(self):
screen = QtGui.QDesktopWidget().screenGeometry()
size = self.geometry()
self.move(50, 0)
def handle(self, event):
if(event.type == 0):
self.map.addNode(event.object.scanner)
if(event.type == 1):
self.map.delNode(event.object.scanner)
if(event.type == 2):
self.map.addBranch(event.object.node1.scanner, event.object.node2.scanner)
if(event.type == 3):
self.map.delBranch(event.object.node1.scanner, event.object.node2.scanner)
if(event.type == 4):
self.map.changeNode(event.object.scanner.sensorid, event.result)
if(event.type == 5):
self.map.changeBranch(event.object.node1.scanner.sensorid, event.object.node2.scanner.sensorid, event.result)
self.repaint(self.map.contentsRect())
self.update(self.map.contentsRect())
######################################################################
class Map(QtGui.QFrame):
def __init__(self, parent, mapimagepath):
QtGui.QFrame.__init__(self, parent)
#self.timer = QtCore.QBasicTimer()
#coordinaten hoeken NE en SW voor kaart in map graphics van SKO
self.realmap = RealMap(
mapimagepath,
(51.0442, 3.7268),
(51.0405, 3.7242),
550,
800)
parent.setGeometry(0,0,self.realmap.width, self.realmap.height)
self.refreshspeed = 5000
self.mapNodes = {}
def addNode(self, scanner):
coord = self.realmap.convertLatLon2Pix((scanner.latitude, scanner.longitude))
self.mapNodes[scanner.sensorid] = MapNode(scanner, coord[0], coord[1])
# type: 4 --> changenode ,
#((change, gem_ref, procentuele verandering ref), scanner object)
def changeNode(self, sensorid, branchdata):
self.mapNodes[sensorid].calcDanger(branchdata[2])
def paintEvent(self, event):
painter = QtGui.QPainter(self)
rect = self.contentsRect()
#teken achtergrond
self.realmap.drawRealMap(painter)
#teken nodes
for sensorid, mapNode in self.mapNodes.items():
mapNode.drawMapNode(painter, self.realmap)
######################################################################
class RealMap:
def __init__(self, path, coordRightTop,
coordLeftBot, width, height, pixpermet = 2.6):
self.path = path
self.coordLeftBot = coordLeftBot
self.coordRightTop = coordRightTop
self.width = width
self.height = height
self.realdim = self.calcRealDim()
self.pixpermet = pixpermet
def drawRealMap(self, painter):
image = QtGui.QImage(self.path)
painter.drawImage(0,0,image)
######################################################################
class MapNode:
dangertocolor = {"normal":"graphics//gradients//green.png",
"elevated":"graphics//gradients//orange.png",
"danger":"graphics//gradients//red.png"}
def __init__(self, scanner, x, y, danger = 0):
self.scanner = scanner
self.x = x
self.y = y
self.danger = 'normal'
self.calcDanger(danger)
def drawMapNode(self, painter, realmap):
radiusm = self.scanner.range
radiusp = radiusm*realmap.pixpermet
factor = radiusp/200 # basis grootte gradiƫnten is 200 pixels.
icon = QtGui.QImage("graphics//BT-icon.png")
grad = QtGui.QImage(MapNode.dangertocolor[self.danger])
grad = grad.scaled(grad.size().width()*factor, grad.size().height()*factor)
painter.drawImage(self.x-100*factor,self.y-100*factor, grad)
painter.drawImage(self.x-10, self.y-10,icon)
painter.drawText(self.x-15, self.y+20, str(self.scanner.sensorid) + '-' + str(self.scanner.name))
An object is made through our application class:
mapview = gfx.MapView(g_image)
mapview.show()
So the first question is. What are we doing wrong in the paintEvent method?
Secondly question
Is there a way to make the paintevent not be called at EVERY RANDOM THING that happens ? (like mouseovers, etc)?
I tried something like:
def paintEvent(self, event):
if(isinstance(event, QtGui.QPaintEvent)):
painter = QtGui.QPainter(self)
rect = self.contentsRect()
#teken achtergrond
self.realmap.drawRealMap(painter)
#teken nodes
for sensorid, mapNode in self.mapNodes.items():
mapNode.drawMapNode(painter, self.realmap)
else:
pass
This 'works' but is to general I guess.. It actually makes the error appear a lot faster then without the conditional.
When in your gfx.py you have:
self.repaint(self.map.contentsRect())
self.update(self.map.contentsRect())
Calling repaint and calling update one right after another is redundant. And if a paint event comes through that handler and you call repaint() there, you are asking for infinite recursion.
Take note of any Warnings or Notes in the documentation.
http://doc.qt.io/qt-4.8/qwidget.html#update
http://doc.qt.io/qt-4.8/qwidget.html#repaint
http://doc.qt.io/qt-4.8/qwidget.html#paintEvent
I don't see the cause for your other error right off, but it probably has to do with QPainter getting used when it shouldn't...
http://doc.qt.io/qt-4.8/qpainter.html#begin
http://doc.qt.io/qt-4.8/qpainter.html#details
Hope that helps.