Drawing line at init method with widget relative coordinates - python

I need to draw a line inside a Widget. But I've seen that the relative position and size of the widget aren't setted until the init method finish.
How can I draw a graphic element inside a widget with his relative position and dimension while I instantiate the class? (I would like to avoid Kv lang)
class Track(Widget):
def __init__(self,**kw):
super(Track, self).__init__(**kw)
with self.canvas:
Color(1,0,0)
Line(points = (self.x, (self.y + self.height) / 2, self.x + self.width, (self.y + self.height) / 2))
In this way the line it's drawn using the initial size and position which are 100,100 and 0,0 but the Widget it's inside a Layout so I'd like to use the relative position and size and I'd like to drawn it in the init

I would like to avoid Kv lang
I recommend discarding this restriction.
In this way the line it's drawn using the initial size and position which are 100,100 and 0,0 but the Widget it's inside a Layout so I'd like to use the relative position and size and I'd like to drawn it in the init
You have three options. The first is to draw it in a clock (kivy.clock.Clock) scheduled function that runs after the widget has been positioned - it should be sufficient to do Clock.schedule_once(the_func, 0), with the 0 deferring the calculation to after the widget is positioned (assuming a normal layout) but before the next frame. The downside is that the line will then be fixed, and won't match the widget if if ever moves, e.g. potentially during window resize.
The second (and better) option is to draw the line as you are now, but bind to the widget pos and size a function that repositions it appropriately. e.g. self.bind(pos=self.line_setter, size=self.line_setter) and have self.line_setter be a method with self.line.points = [...] as appropriate. You would also need to save a reference to the line with self.line = Line(...).
The third (and normally best) option is to use kv language, which automatically creates the bindings for you with no additional syntax.

Demo of Inclement's Brillant answer!
The goal is to reposition a figure to the center of the widget
from kivy.app import App
from kivy.graphics.vertex_instructions import Line
class MyWidget(Widget):
def __init__(self):
super().__init__()
print(f"on init: {self.width}, {self.height}")
with self.canvas:
self.coordinate = Translate(0, 0)
Line(points=[0,0,100,100], width=20)
#Comment the following line
self.bind(pos=self.reposition, size=self.reposition)
def reposition(self, *args):
print(f"on reposition: {self.width}, {self.height}")
self.coordinate.xy = self.width/2, self.height/2
class myApp(App):
def build(self):
return MyWidget()
myApp().run()
Results:
on init: 100, 100
on reposition: 1000, 500
Before|After binding:

Related

PyQt5 left click not working for mouseMoveEvent

I'm trying to learn PyQt5, and I've got this code:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.label = QLabel()
canvas = QPixmap(400, 300)
canvas.fill(Qt.white)
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
def mouseMoveEvent(self, e):
painter = QPainter(self.label.pixmap())
painter.drawPoint(e.x(), e.y())
painter.end()
self.update()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
And I can draw using right click to draw, but when I left click, it drags the window instead of drawing. This even happens when I make the window fullscreen so I can't move it. How can I stop it from dragging the window so it will draw instead?
In some configurations (specifically, on Linux, and depending on the window manager settings), dragging the left mouse button on an empty (non interactive) area of a QMainWindow allows dragging the whole window.
To prevent that, the mouse move event has to be accepted by the child widget that receives it.
While this can be achieved with an event filter, it's usually better to use a subclass, and this is even more important whenever the widget has to deal with mouse events it receives, exactly like in this case.
Another aspect that has to be considered is that just updating the QLabel pixmap is not completely sufficient, because it doesn't automatically force its update. Also, since Qt 5.15, QLabel.pixmap() doesn't return a pointer to the pixmap, but rather its copy. This means that you should always keep a local reference to the pixmap for the whole time required to access it (otherwise your program will crash), and then call setPixmap() again with the updated pixmap after "ending" the painter. This will automatically schedule an update of the label.
The above may be a bit confusing if you're not used to languages that allow pointers as arguments, but, in order to clarify how it works, you can consider the pixmap() property similarly to the text() one:
text = self.label.text()
text += 'some other text'
The above will obviously not change the text of the label, most importantly because, in most languages (including Python) strings are always immutable objects, so text += ... actually replaces the text reference with another string object.
To clarify, consider the following:
text1 = text2 = self.label.text()
text1 += 'some other text'
print(text1 == text2)
Which will return False.
Now consider this instead:
list1 = list2 = []
list1 += ['item']
print(list1 == list2)
Which will return True, because list is a mutable type, and in python changing the content of a mutable type will affect any reference to it[1], since they refer to the same object.
Until Qt < 5.15, the pixmap of QLabel behaved similarly to a list, meaning that any painting on the label.pixmap() would actually change the content of the displayed pixmap (requiring label.update() to actually show the change). After Qt 5.15 this is no longer valid, as the returned pixmap behaves similarly to a returned string: altering its contents won't change the label's pixmap.
So, the proper way to update the pixmap is to:
handle the mouse event in the label instance (either by subclassing or using an event filter), and not in a parent;
get the pixmap, keep its reference until painting has completed, and call setPixmap() afterwards (mandatory since Qt 5.15, but also suggested anyway);
Finally, QLabel has an alignment property that, when using a pixmap, is used to set the alignment of the pixmap to the available space that the layout manager provides. The default is left aligned and vertically centered (Qt.AlignLeft|Qt.AlignVCenter).
QLabel also features the scaledContents property, which always scales the pixmap to the current size of the label (not considering the aspect ratio).
The above means one of the following:
the pixmap will always be displayed at its actual size, and eventually aligned within its available space;
if the scaledContents property is True, the alignment is ignored and the pixmap will be always scaled to the full extent of its available space; whenever that property is True, the resulting pixmap is also cached, so you have to clear its cache every time (at least, with Qt5);
if you need to always keep aspect ratio, using QLabel is probably pointless, and you may prefer a plain QWidget that actively draws the pixmap within a paintEvent() override;
Considering the above, here is a possible implementation of the label (ignoring the ratio):
class PaintLabel(QLabel):
def mouseMoveEvent(self, event):
pixmap = self.pixmap()
if pixmap is None:
return
pmSize = pixmap.size()
if not pmSize.isValid():
return
pos = event.pos()
scaled = self.hasScaledContents()
if scaled:
# scale the mouse position to the actual pixmap size
pos = QPoint(
round(pos.x() * pmSize.width() / self.width()),
round(pos.y() * pmSize.height() / self.height())
)
else:
# translate the mouse position depending on the alignment
alignment = self.alignment()
dx = dy = 0
if alignment & Qt.AlignRight:
dx += pmSize.width() - self.width()
elif alignment & Qt.AlignHCenter:
dx += round((pmSize.width() - self.width()) / 2)
if alignment & Qt.AlignBottom:
dy += pmSize.height() - self.height()
elif alignment & Qt.AlignVCenter:
dy += round((pmSize.height() - self.height()) // 2)
pos += QPoint(dx, dy)
painter = QPainter(pixmap)
painter.drawPoint(pos)
painter.end()
# this will also force a scheduled update
self.setPixmap(pixmap)
if scaled:
# force pixmap cache clearing
self.setScaledContents(False)
self.setScaledContents(True)
def minimumSizeHint(self):
# just for example purposes
return QSize(10, 10)
And here is an example of its usage:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.label = PaintLabel()
canvas = QPixmap(400, 300)
canvas.fill(Qt.white)
self.label.setPixmap(canvas)
self.hCombo = QComboBox()
for i, hPos in enumerate(('Left', 'HCenter', 'Right')):
hAlign = getattr(Qt, 'Align' + hPos)
self.hCombo.addItem(hPos, hAlign)
if self.label.alignment() & hAlign:
self.hCombo.setCurrentIndex(i)
self.vCombo = QComboBox()
for i, vPos in enumerate(('Top', 'VCenter', 'Bottom')):
vAlign = getattr(Qt, 'Align' + vPos)
self.vCombo.addItem(vPos, vAlign)
if self.label.alignment() & vAlign:
self.vCombo.setCurrentIndex(i)
self.scaledChk = QCheckBox('Scaled')
central = QWidget()
mainLayout = QVBoxLayout(central)
panel = QHBoxLayout()
mainLayout.addLayout(panel)
panel.addWidget(self.hCombo)
panel.addWidget(self.vCombo)
panel.addWidget(self.scaledChk)
mainLayout.addWidget(self.label)
self.setCentralWidget(central)
self.hCombo.currentIndexChanged.connect(self.updateLabel)
self.vCombo.currentIndexChanged.connect(self.updateLabel)
self.scaledChk.toggled.connect(self.updateLabel)
def updateLabel(self):
self.label.setAlignment(Qt.AlignmentFlag(
self.hCombo.currentData() | self.vCombo.currentData()
))
self.label.setScaledContents(self.scaledChk.isChecked())
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Note that if you need more advanced control over the pixmap display and painting (including aspect ratio, but also proper zoom capabilities and any possible complex feature), then the common suggestion is to completely ignore QLabel, as said above: either use a basic QWidget, or consider the more complex (but much more powerful) Graphics View Framework. This will also allow proper editing features, as you can add non-destructive editing that will show ("paint") the result without affecting the actual, original object.
[1]: The above is based on the fact that a function or operator can actually mutate the object: the += operator actually calls the __add__ magic method that, in the case of lists, updates the contents of the same list.

How to position labels with move command based on their center instead of left corner in PyQt5

Hi I have several QLabels in a Qwidget. I am given a design that I need to create in Qt and it has only few labels actually. But one of labels' text my change. So text length is also changable. I used move() command it takes left corner point as reference point. I need to take center point of Label as reference point I guess.
class App(QWidget):
def __init__(self):
super().__init__()
self.left = 0
self.top = 0
self.width = 480
self.height = 800
self.initUI()
def initUI(self):
#self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
# Create widget
main_page = QLabel(self)
main_pixmap = QPixmap('main_page')
main_page.setPixmap(main_pixmap)
#logo
logo = QLabel(self)
logo_pixmap = QPixmap('logo')
logo.setPixmap(logo_pixmap)
logo.move(159,63)
#Date
today = format_date(datetime.datetime.now(),"dd MMMM", locale = 'tr').upper()
day = format_date(datetime.datetime.now(), "EEEE", locale = 'tr').upper()
date = QLabel(self)
date.setText(day + " " + today )
date.setFont(QFont("Lato", 24))
date.setStyleSheet('color:white')
self.move_to_center()
#Clock
clock = QLabel(self)
clock.setText(strftime('%H:%M'))
clock.setFont(QFont("Lato", 90))
clock.setStyleSheet('color:white')
clock.move(71,222)
How can I dynamicly put a label horizantally middle of a Qwidget?
Edit:
When I used layouts, labels lines up one after another as below
The main problem with centering widgets is that they can alter their size according to their contents.
The simpler solution for your case is to create a label that has a fixed width and has its text center aligned:
clock = QLabel(self)
clock.setText(strftime('%H:%M'))
clock.setFixedWidth(self.width())
clock.move(0, 222)
clock.setAlignment(Qt.AlignCenter)
While this is fine, there is a couple of problems:
if the label has more than one line and you don't want it to span over the whole width, the alignment will always be centered (which can be ugly);
if the original text is on one line and the new text has more, it won't be updated properly (unless you call label.adjustSize() after every text change)
Another solution is to create a subclass of QLabel that automatically repositions itself as soon as it's shown or the text is changed:
class CenterLabel(QLabel):
vPos = 0
def setText(self, text):
super(CenterLabel, self).setText(text)
if self.parent():
self.center()
def setVPos(self, y):
# set a vertical reference point
self.vPos = y
if self.parent():
self.center()
def center(self):
# since there's no layout, adjustSize() allows us to update the
# sizeHint based on the text contents; we cannot use the standard
# size() as it's unreliable when there's no layout
self.adjustSize()
x = (self.parent().width() - self.sizeHint().width()) / 2
self.move(x, self.vPos)
That said, I still think that using a layout is a better and simpler solution. You just have to create the "background" pixmap with the main widget as a parent and without adding it to the layout, then set the layout and add everything else using layout.addSpacing for the vertical spacings between all widgets.
The only issue here is that if a label text changes its line count, all subsequent widgets will be moved accordingly. If that's the case, just set a fixed height for the widget which will be equal to the distance between the top of the widget and the top of the next, then add the widget to the layout ensuring that it is horizontally centered and top aligned.
def initUI(self):
#self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self._width, self._height)
self.background = QLabel(self)
self.background.setPixmap(QPixmap('background.png'))
layout = QVBoxLayout()
self.setLayout(layout)
# set spacing between items to 0 to ensure that there are no added margins
layout.setSpacing(0)
# add a vertical spacing for the top margin;
# I'm just using random values
layout.addSpacing(20)
main_page = QLabel(self)
main_pixmap = QPixmap('main_page')
main_page.setPixmap(main_pixmap)
# add the widget ensuring that it's horizontally centered
layout.addWidget(main_page, alignment=Qt.AlignHCenter)
#logo
logo = QLabel(self)
logo_pixmap = QPixmap('logo')
logo.setPixmap(logo_pixmap)
layout.addWidget(logo, alignment=Qt.AlignHCenter)
layout.addSpacing(50)
#Date
today = format_date(datetime.datetime.now(),"dd MMMM", locale = 'tr').upper()
day = format_date(datetime.datetime.now(), "EEEE", locale = 'tr').upper()
date = QLabel(self)
date.setText(day + " " + today )
date.setFont(QFont("Lato", 24))
date.setStyleSheet('color:white')
# set a fixed height equal to the vertical position of the next label
date.setFixedHeight(100)
# ensure that the label is always on top of its layout "slot"
layout.addWidget(date, alignment=Qt.AlignHCenter|Qt.AlignTop)
#Clock
clock = QLabel(self)
clock.setText(strftime('%H:%M'))
clock.setFont(QFont("Lato", 90))
clock.setStyleSheet('color:white')
layout.addWidget(clock, alignment=Qt.AlignHCenter|Qt.AlignTop)
# add a bottom "stretch" to avoid vertical expanding of widgets
layout.addStretch(1000)

Kivy places widgets slightly differently through Python and Kivy language. Am I missing something?

I just picked up Kivy and encountered this problem. If there is a better way to achieve what I'm trying to in general I'd love to hear about it, though.
What I've noticed is that, when I add a widget to another widget, if I do so through Python code it will be slightly at a different position than had I done so through Kivy. I'll paste my code below (it's pretty short right now) and you can just try it yourself and you'll see what I mean.
client.py:
import kivy
kivy.require('1.9.1') # current kivy version
from kivy.config import Config
Config.set('graphics', 'width', '360')
Config.set('graphics', 'height', '640')
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty, NumericProperty, ReferenceListProperty
from kivy.graphics import Color, Rectangle
from kivy.clock import Clock
from kivy.vector import Vector
from random import randint
class Bird(Widget):
'''
Bird Widget defines each sprite. / Test version: blue and red cards
Defined attributes:
SIZE
POSITION
(COLOR)
Upade the position of this widget individually every 0.7 seconds with 60 fps
'''
# set attributes
border_color = (1,1,1)
r = NumericProperty(0)
g = NumericProperty(0)
b = NumericProperty(0)
color = ReferenceListProperty(r, g, b) # initial color = red // maybe make it random
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(-3)
velocity = ReferenceListProperty(velocity_x, velocity_y)
def __init__(self, **kwargs):
super(Bird, self).__init__(**kwargs)
self.pick_color()
# Randomly generate 0 or 1, and pick a color based on that
def pick_color(self):
color_num = randint(0,1)
if color_num == 0: # blue
self.color = (0,0,1)
elif color_num == 1: # red
self.color = (1,0,0)
# Move the widget by -3y increment at 60 fps
def increment(self, dt):
self.pos = Vector(*self.velocity) + self.pos
def move(self):
# While the sprite moves at 60 fps, the movement is "cancelled" after 0.3 seconds
# This event sequence is refreshed every 0.7 seoncds in MainApp class
move = Clock.schedule_interval(self.increment, 1.0/60.0)
stop = Clock.schedule_once(lambda dt: move.cancel(), 0.3)
class GameMain(Widget):
'''
Contains two functions: ADD_NEW_BIRD() and UPDATE().
All controls happen in this widget
Not using kivy.screen because there is only one screen used
ADD_NEW_BIRD() adds a new bird to list_of_birds AND add it as a child widget to the GameMain Widget. UPDATE() calls MOVE() (Bird Widget) and receives events
Create global variable limit = 0; if limit == 4, game over; variable accessed in update function, which checks whether the limit has been reached. If the player makes the right decision, then limit -= 1
'''
limit = 0
def add_new_bird(self):
self.new_bird = Bird(center_x=self.center_x, center_y=self.height/1.5)
print (self.center_x, self.height)
self.new_bird.pick_color()
self.add_widget(self.new_bird)
def update(self, dt):
for bird in self.children:
bird.move()
self.add_new_bird()
class MainApp(App):
def build(self):
game = GameMain()
Clock.schedule_interval(game.update, 0.7)
return game
if __name__ == '__main__':
MainApp().run()
main.kv:
#:kivy 1.9
<Bird>:
size: 70, 80
canvas:
Color:
rgb: self.border_color
Rectangle:
size: self.size
pos: self.pos
Color:
rgb: self.color
Rectangle:
size: root.width - 10, root.height - 10
pos: root.x + 5, root.y + 5
<GameMain>
Bird:
center_x: root.center_x
center_y: root.height / 1.5
The code does exactly what I want it to do (I'm going to touch on the z-values later) except that the very first card is slightly off to the left. I'm just really confused because center_x: root.center_x in main.kv should not be any different from Bird(center_x=self.center_x in client.py as far as I understand. I've tried initializing the first instance of Bird() inside of an init function like so:
def __init__(self, **kwargs):
super(GameMain, self).__init__(**kwargs)
self.bird = Bird(center_x=self.center_x, center_y=self.height/1.5)
self.bird.pick_color()
self.add_widget(self.bird)
And the problem was still there! If anyone could explain what's going on/what I'm doing wrong and maybe even suggest a better way to approach this, I'd appreciate it.
Just in case you're curious, I need to add widgets directly from Python code because I need the app to constantly produce a new card at a constant time interval. The first card, however, is initialized in the Kivy file for the sake of simplicity. To be fair it works pretty well except for the offset. And lastly I'm not using a Layout because I wasn't sure which one to use... I did lay my hands on FloatLayout for a bit but it didn't seem like it was going to fix my problem anyway.
When constructed, Widget has an initial size of (100, 100). If you change size from this:
<Bird>:
size: 70, 80
to this:
<Bird>:
size: 100, 80
rectangles will align correctly. Initial rectangle, created in kv file, is centered at the parent window, other ones that are created in Python code are offset to the left.
If you change Bird constructor in Python code from this:
def __init__(self, **kwargs):
super(Bird, self).__init__(**kwargs)
self.pick_color()
to this (effectively overriding the default widget size from (100, 100) to be (50,50)):
def __init__(self, **kwargs):
self.size = (50, 50)
super(Bird, self).__init__(**kwargs)
self.pick_color()
you'll notice that rectangles created in Python code will shift to the right. Change kv file from:
<Bird>:
size: 70, 80
to:
<Bird>:
size: 50, 80
which matches (new) initial widget size of (50,50) in width, all rectangles will be aligned again.
Solution to your problem would be to leave all as is, except to set size for new birds in Python to be equal to that in kv file:
def __init__(self, **kwargs):
self.size = (70, 80)
super(Bird, self).__init__(**kwargs)
self.pick_color()
and all will work as intended.
This all means that size property from kv file is not applied to your Python-side created Birds, only to the one created by kv declaration. Is this Kivy bug or maybe you are missing one more step in the Python code to make Builder apply size from kv file to Python-created Birds, I have no idea right now.
In my experience, at this point of Kivy development, mixing too much kv and Python code will result in these kind of weird issues you have here. It is best to either handle all view related stuff in kv or to ditch kv completely and build everything in Python.
Some things don't work at all in kv, i.e. setting cols property of GridLayout (v1.9.1).
Personally, for now, I stick to well organized Python code to build UI and don't use kv files almost at all.
Hope this helps a bit...

Qgraphicsview items not being placed where they should be

I recently created a program that will create QgraphicsEllipseItems whenever the mouse is clicked. That part works! However, it's not in the exact place where my cursor is. It seems to be slightly higher than where my mouse cursor is. I do have a QGraphicsRectItem created so maybe the two items are clashing with each other and moving off of one another? How can I get these circles to be placed on top of the rectangle item? Here's the code
class MyView(QtGui.QGraphicsView):
def __init__(self):
QtGui.QGraphicsView.__init__(self)
self.scene = QtGui.QGraphicsScene(self)
self.item = QtGui.QGraphicsRectItem(400, 400, 400, 400)
self.scene.addItem(self.item)
self.setScene(self.scene)
def paintMarkers(self):
self.cursor = QtGui.QCursor()
self.x = self.cursor.pos().x()
self.y = self.cursor.pos().y()
self.circleItem = QtGui.QGraphicsEllipseItem(self.x,self.y,10,10)
self.scene.addItem(self.circleItem)
self.circleItem.setPen(QtGui.QPen(QtCore.Qt.red, 1.5))
self.setScene(self.scene)
class Window(QtGui.QMainWindow):
def __init__(self):
#This initializes the main window or form
super(Window,self).__init__()
self.setGeometry(50,50,1000,1000)
self.setWindowTitle("Pre-Alignment system")
self.view = MyView()
self.setCentralWidget(self.view)
def mousePressEvent(self,QMouseEvent):
self.view.paintMarkers()
Much thanks!
There are two issues with the coordinates you are using to place the QGraphics...Items. The first is that the coordinates from QCursor are global screen coordinates, so you need to use self.mapFromGlobal() to convert them to coordinates relative to the QGraphicsView.
Secondly, you actually want the coordinates relative to the current QGraphicsScene, as this is where you are drawing the item. This is because the scene can be offset from the view (for example panning around a scene that is bigger than a view). To do this, you use self.mapToScene() on the coordinates relative to the QGraphicsView.
I would point out that typically you would draw something on the QGraphicsScene in response to some sort of mouse event in the QGraphicsView, which requires reimplementing things like QGraphicsView.mouseMoveEvent or QGraphicsView.mousePressEvent. These event handlers are passed a QEvent which contains the mouse coordinates relative to the view, and so you don't need to do the global coordinates transformation I mentioned in the first paragraph in these cases.
Update
I've just seen your other question, and now understand some of the issue a bit better. You shouldn't be overriding the mouse event in the main window. Instead override it in the view. For example:
class MyView(QtGui.QGraphicsView):
def __init__(self):
QtGui.QGraphicsView.__init__(self)
self.scene = QtGui.QGraphicsScene(self)
self.item = QtGui.QGraphicsRectItem(400, 400, 400, 400)
self.scene.addItem(self.item)
self.setScene(self.scene)
def paintMarkers(self, event):
# event position is in coordinates relative to the view
# so convert them to scene coordinates
p = self.mapToScene(event.x(), event.y())
self.circleItem = QtGui.QGraphicsEllipseItem(0,0,10,10)
self.circleItem.setPos(p.x()-self.circleItem.boundingRect().width()/2.0,
p.y()-self.circleItem.boundingRect().height()/2.0)
self.scene.addItem(self.circleItem)
self.circleItem.setPen(QtGui.QPen(QtCore.Qt.red, 1.5))
# self.setScene(self.scene) # <-- this line should not be needed here
# Note, I've renamed the second argument `event`. Otherwise you locally override the QMouseEvent class
def mousePressEvent(self, event):
self.paintMarkers(event)
# you may want to preserve the default mouse press behaviour,
# in which case call the following
return QGraphicsView.mousePressEvent(self, event)
Here we have not needed to use QWidget.mapFromGlobal() (what I covered in the first paragraph) because we use a mouse event from the QGraphicsView which returns coordinates relative to that widget only.
Update 2
Note: I've updated how the item is created/placed in the above code based on this answer.

Qt still calls .paint() on QGraphicsItem despite caching enabled and no calls to .update()

I've been trying to draw many 'rects' efficiently in Qt (PySide), yet it still appears to lag drawing the entire 'grid' in the paint call of a QGraphicsItem.
class GridMapView(QObject, QGraphicsItem):
def __init__(self, gridMap, mapWidth, mapHeight, cellSize):
QObject.__init__(self)
QGraphicsItem.__init__(self)
self.setCacheMode(QGraphicsItem.ItemCoordinateCache)
self.gridMap = gridMap
self.cellSize = cellSize
self.width = mapWidth
self.height = mapHeight
self.setPos(-self.width/2, -self.height/2)
def boundingRect(self):
return QRectF(0, 0, self.width, self.height)
def paint(self, painter, option, widget):
painter.setPen(Qt.NoPen)
unknownBrush = QBrush(QColor('grey'))
freeBrush = QBrush(QColor('white'))
occupiedBrush = QBrush(QColor('black'))
cellRect = QRectF()
for ix, col in enumerate(self.gridMap):
for iy, cell in enumerate(col):
if cell == CellStates.UNKNOWN:
painter.setBrush(unknownBrush)
elif cell == CellStates.FREE:
painter.setBrush(freeBrush)
elif cell == CellStates.OCCUPIED:
painter.setBrush(occupiedBrush)
cellRect.setRect(ix*self.cellSize, iy*self.cellSize, self.cellSize, self.cellSize)
painter.drawRect(cellRect)
This is rendering a few thousand rects, and lags a lot. Setting the cache mode (and making sure not to move the view) doesn't appear to help.
My assumption was that painting the entire grid in one pass would be efficient, if it only gets redrawn when one cell changes.
Am I missing something fundamental here? Thanks.
Ensure you set QGraphicsView.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate) on the view containing the scene with that item. In either case, when you pass (hover) your mouse accross a GridMapView item, its paint() is called, so all the squares are repainted in your case.
An alternative implementation would be to make each square an individual QGraphicsItem so that, provided above viewport update mode is set on the view, only those items are repainted that need be.

Categories

Resources