In my QT application I'm drawing lots of polygons like this:
I'm animating these, so some polygons will receive a new color. This animation runs 4-5 times per second.
However, calling the paintEvent() of the Qt.Painter() 4-5 times/second redraws ALL polygons which results in performance issues. Its only updated once a second, which is too slow. As you may see in the picture below, only some polygons in the first 12 rows needs to be updated:
[![enter image description here][2]][2]
In the QT docs I have read that you can't really save the state of the things you've already drawn. So you have to redraw everything again. Am I missing something? Is there a trick to still achieve this?
This is what my paintEvent() basically looks like (simplified, reduced cyclomatic complexity)
for y in range(len(self.array)):
for x in range(len(self.array[0])):
if(this): # simplified to reduce cyclomatic complexity
painter.setBrush(QBrush(QColor(20, 0, 255)))
elif(that):
painter.setBrush(QBrush(QColor(175, 175, 175)))
else:
painter.setBrush(QBrush(QColor(0, 0, 0)))
hexa_size = self.array[y][x]
hexas = createHexagon(x, y, hexa_size) # external functions to calculate the hexagon size and position
painter.drawPolygon(hexas)
painter.end()
call (update on each Pin change):
while True:
while(stempel.readPin(0) == 0):
QApplication.processEvents()
time.sleep(0.01)
self.draw_area.update() # Pin state changed, update polygons
while(stempel.readPin(0) == 1):
QApplication.processEvents()
time.sleep(0.01)
Qt allows scheduling an update for only a portion (region) of the widget, thus optimizing the result. This requires two step:
calling update(QRect) with an appropriate rectangle that covers only the part of the widget that requires repainting;
checking the event.rect() and then implement painting in order to paint only that region;
If you know for sure that only the first X rows are going to change color, then:
self.draw_area.update(
QRect(0, 0, self.draw_area.width(), <height of the repainted rows>)
Then, in the paintEvent:
if event.rect().bottom() < <height of the repainted rows>:
rowRange = range(indexOfTheLastRowToRepaint + 1)
else:
rowRange = range(len(self.array))
Note that another solution could be using QPicture, which is a way to "serialize" a QPainter in order to improve performance and avoid unnecessary computations.
class DrawArea(QWidget):
cache = None
def paintEvent(self, event):
if not self.cache:
self.cache = QPicture()
cachePainter = QPainter(self.cache)
# draw on the painter
cachePainter.end()
painter = QPainter(self)
painter.drawPicture(0, 0, self.cache)
def resizeEvent(self, event):
self.cache = None
The code above is very minimalistic, you might create multiple QPictures for every group of row and then decide which one paint whenever you require it, even by combining the event.rect() checking as explained above.
The major benefit of this technique is that QPainter usually processes a QPicture pretty fast, so you don't have to do all computations required for rows, polygons, etc.
Finally, the image you provided seems very repetitive, almost like a texture. In that case, you might consider using a QPixmap for each group of rows and then create a QBrush with that QPixmap. In that case, you'll only need to call painter.fillRect(self.rect(), self.textureBrush).
Solved it myself by using a QGraphicsScene + QGraphicsView:
self.scene = QGraphicsScene()
self.graphicView = QGraphicsView(self.scene, self)
Creating a list where all polygons are being saved:
self.polygons = [ [0] * len(array[0]) for _ in range(len(array))]
Initial drawing of all polygons:
for y in range(len(array)):
for x in range(len(array[0])):
polygon_size = self.array[y][x]
polygon = createPoly(x, y, polygon_size)
self.polygons[y][x] = self.scene.addPolygon(polygon, QPen(Qt.NoPen), QBrush(Qt.black))
if(y % 50 == 0): QApplication.processEvents()
Update rows indivudually:
for poly_size in active_rows:
for active_row in active_rows[poly_size]:
for x in range(0, len(array[0])):
if(array[active_row][x] == int(poly_size)):
self.polygons[active_row][x].setBrush(QBrush(QColor(20, 0, 255)))
if(array[active_row - 2][x] > 0 and array[active_row - 2][x] == int(poly_size)):
self.polygons[active_row - 2][x].setBrush(QBrush(QColor(175, 175, 175)))
Related
I'm trying to make a Qlabel that will only fill available space in Qt (really in PyQt but the same principle applies). It's meant to display a (sometimes really long) file path, and as I currently have it the length of the label makes the minimum size of that part of the window way too large.
I want to make the label's text be reduced to the maximum possible length without exceeding the minimum width due to the other widgets in the panel. I have found the QFontMetrics::elideText() method, which can effectively clip the text in the way I want, but I still can't figure out how to get the pixel width without the label affecting the size of the panel.
My hackjob thought process is to set the Qlabel's sizes to zero by overriding size/minimumsize/maximumsize, measure the remaining space allotted, and then set text to that. However, I don't know how to get that remaining space, and I feel like there should be a better way.
My layout for reference:
QLabel is a pretty neat widget: it seems very simple, but it's not.
The size and displaying aspects are very important: since it's able to display formatted text, it can even have some layout issues.
Since your requirement is to keep the label as small as possible (but while keeping its content displayed if possible), the most important requirement is to implement the sizeHint (and minimumSizeHint()) functions, since the layout of the parent widget will consider that when resizing its contents.
A possible solution is based on two aspects:
provide a basic [minimum] size hint that doesn't consider the whole contents
override the painting by eliding text whenever the available space is not enough
The following code obviously does NOT consider rich text formatting, including different paragraph alignment, word wrapping, etc.
This is an example showing a subclassed QLabel trying to display the following path:
'/tmp/test_dir/some_long_path/some_subdir/imagepath/'
Consider that you could even actually use a basic QWidget instead. In the following code I'm considering the QFrame subclassing abilities which also include adding proper margins and borders, depending on the style and the frameShape or frameShadow properties.
class ElideLabel(QtWidgets.QLabel):
_elideMode = QtCore.Qt.ElideMiddle
def elideMode(self):
return self._elideMode
def setElideMode(self, mode):
if self._elideMode != mode and mode != QtCore.Qt.ElideNone:
self._elideMode = mode
self.updateGeometry()
def minimumSizeHint(self):
return self.sizeHint()
def sizeHint(self):
hint = self.fontMetrics().boundingRect(self.text()).size()
l, t, r, b = self.getContentsMargins()
margin = self.margin() * 2
return QtCore.QSize(
min(100, hint.width()) + l + r + margin,
min(self.fontMetrics().height(), hint.height()) + t + b + margin
)
def paintEvent(self, event):
qp = QtGui.QPainter(self)
opt = QtWidgets.QStyleOptionFrame()
self.initStyleOption(opt)
self.style().drawControl(
QtWidgets.QStyle.CE_ShapedFrame, opt, qp, self)
l, t, r, b = self.getContentsMargins()
margin = self.margin()
try:
# since Qt >= 5.11
m = self.fontMetrics().horizontalAdvance('x') / 2 - margin
except:
m = self.fontMetrics().width('x') / 2 - margin
r = self.contentsRect().adjusted(
margin + m, margin, -(margin + m), -margin)
qp.drawText(r, self.alignment(),
self.fontMetrics().elidedText(
self.text(), self.elideMode(), r.width()))
You can override the paintEvent() and achieve it in the following way:
class QElidedLabel(QLabel):
def paintEvent(self, event):
painter = QPainter(self)
textDoc = QTextDocument()
metrics = QFontMetrics(self.font())
elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width() - 10)
textDoc.setPlainText(elided)
textDoc.drawContents(painter)
This will draw the Elided Label automatically and you won't have to change your code anywhere else. Also you can set the size policy of QLabel to MinimumExpanding to make sure your QLabel takes the maximum available space. This way the self.width() returns the current maximum width. You can take a look at documentation for the working of QTextDocument() and QFontMetrics(). Also, self.width() - 10 just makes sure that '...' in the elided label is not hidden, you can remove - 10 and just use self.width() if .. visible for you after removing it as well.
I have an object that changes its display based on which way it's facing. The object takes a 4x4 grid of frames, and uses each row of 4 frames as an animation for each state.
Currently, I'm loading these into separate sprites using:
def create_animation(image_grid, start_idx, end_idx):
frames = []
for frame in image_grid[start_idx:end_idx]:
frames.append(pyglet.image.AnimationFrame(frame, 0.1))
return pyglet.sprite.Sprite(pyglet.image.Animation(frames))
and then adding the sprite that should be displayed to a batch to be drawn, and removing it when it shouldn't be drawn.
However, reading the documentation, I saw this:
Sprite.batch
The sprite can be migrated from one batch to another, or removed from its batch (for individual drawing). Note that this can be an expensive operation.
Is there a better way to achieve what I'm trying to do without the performance hit of switching the individual sprites in and out of batches?
You can load the image as a TextureGrid:
img = pyglet.resource.image("obj_grid.png")
img_grid = pyglet.image.ImageGrid(
img,
4, # rows, direction
4 # cols, frames of the animation
)
texture_grid = pyglet.image.TextureGrid(img_grid) # this is the one you actually use
Create your (single) sprite:
batch = pyglet.graphics.Batch() # unless you already have a batch
my_object = pyglet.sprite.Sprite(
img=texture_grid[0, 0], # [row, col] for starting grid /frame
batch=batch
)
Determine/change direction ("row", I'm guessing based on input?).
Loop over 0-3 ("col"/frame of the animation ):
pyglet.clock.schedule_interval(change_frame, 0.1, my_object)
def change_frame(dt, my_object): # pyglet always passes 'dt' as argument on scheduled calls
my_object.col += 1
my_object.col = my_object.col & 3 # or my_object.col % 3 if its not a power of 2
And set the frame manually:
current_frame = self.texture_grid[self.row, self.col].get_texture()
my_object._set_texture(current_frame)
No additional calls to draw(), no messing with the batch(). Everything is drawn as usual, but you change the texture it draws as you wish :)
I'm trying to create a GUI for a virtual board for the game Go. There should be an nxn grid of tiles where a player can place a stone, either black or white. Clicking on a tile will make it change from tan(the default) to black, click again to white, and click a third time to go back to tan. Player one can click once on a spot to place a stone there, and player two can click twice (you need to remove stones later, so three clicks resets it). I created a tile object and then used a nested for loop to instantiate 9 by 9 of them. Unfortunately, running the code only seems to produce 1 functional tile, not 81. This code should work on any python machine (I'm using Python 3.4), so you can try to run it and see for yourself. Can anyone point out the reason the loop is only running once?
from tkinter import *
window = Tk()
n = 9
"""
A tile is a point on a game board where black or white pieces can be placed. If there are no pieces, it remains tan.
The basic feature is the "core" field which is a tkinter button. when the color is changed, the button is configured to represent this.
"""
class tile(object):
core = Button(window, height = 2, width = 3, bg = "#F4C364")
def __init__(self):
pass
"""the cycle function makes the tile object actually change color, going between three options: black, white, or tan."""
def cycle(self):
color = self.core.cget("bg")
if(color == "#F4C364"): #tan, the inital value.
self.core.config(bg = "#111111")#white.
elif (color == "#111111"):
self.core.config(bg = "#DDDDDD")#black.
else:
self.core.config(bg = "#F4C364")#back to tan.
board = [] #create overall array
for x in range(n):
board.append([])#add subarrays inside it
for y in range(n):
board[x].append(tile())#add a tile n times in each of the n subarrays
T = board[x][y] #for clarity, T means tile
T.core.config(command = lambda: T.cycle()) #I do this now because cycle hadn't been defined yet when I created the "core" field
T.core.grid(row = x, column = y) #put them into tkinter.
window.mainloop()
As mhawke points out in his answer you need to make the core an instance variable, so that each Tile gets its own core.
And as I mention in my comment above, you also need to fix the Button's command callback function. The code you use in your question will call the .cycle() method of the current value of T, which happens to be the last tile created. So no matter where you click only the last tile changes color. One way to fix that is to pass the current tile as a default argument of the lambda function when you create it. But because you are using OOP to create your Tile there's a better way, which you can see below.
I've made a few modifications to your code.
Although many Tkinter examples use from tkinter import * it's not a good practice. When you do from some_module import * it brings all of the names from some_module into the current module (your script), which means you could accidentally override those names with your own names. Even worse, if you do import * with multiple modules each new module's names can clash with the previous module's names, and you have no way of knowing that's happened until you start getting mysterious bugs. Using import tkinter as tk means you need to do a little more typing, but it makes the resulting program less bug-prone and easier to read.
I've modified the __init__ method so that it is called with the window and the (x, y) location of the tile (it's customary to use x for the horizontal coordinate and y for the vertical coordinate). Each Tile object now keeps track of its current state, where 0=empty, 1=black, 2=white. This makes it easier to update the colors. And because we've passed in the window and (x, y) we can use that info to add the tile to the grid. The tile also remembers the location (in self.location), which may come in handy.
I've modified the cycle method so that it updates both the background color and the activebackground of the tile. So when the mouse hovers over the tile it changes to a color that's (roughly) halfway between its current color and the color it will turn if you click it. IMO, this is nicer than the tile always turning pale grey when the mouse hovers over it.
I've also optimized the code that creates all the tiles and stores them in the board list of lists.
import tkinter as tk
colors = (
#background, #activebackground
("#F4C364", "#826232"), #tan
("#111111", "#777777"), #black
("#DDDDDD", "#E8C8A8"), #white
)
class Tile(object):
""" A tile is a point on a game board where black or white pieces can be placed.
If there are no pieces, it remains tan.
The basic feature is the "core" field which is a tkinter button.
when the color is changed, the button is configured to represent this.
"""
def __init__(self, win, x, y):
#States: 0=empty, 1=black, 2=white
self.state = 0
bg, abg = colors[self.state]
self.core = tk.Button(win, height=2, width=3,
bg=bg, activebackground=abg,
command=self.cycle)
self.core.grid(row=y, column=x)
#self.location = x, y
def cycle(self):
""" the cycle function makes the tile object actually change color,
going between three options: black, white, or tan.
"""
#cycle to the next state. 0 -> 1 -> 2 -> 0
self.state = (self.state + 1) % 3
bg, abg = colors[self.state]
self.core.config(bg=bg, activebackground=abg)
#print(self.location)
window = tk.Tk()
n = 9
board = []
for y in range(n):
row = [Tile(window, x, y) for x in range(n)]
board.append(row)
window.mainloop()
The problem is that core is a class variable which is created once and shared by all instances of class tile. It should be an instance variable for each tile instance.
Move core = Button(window, height = 2, width = 3, bg = "#F4C364") into tile.__init__() like this:
class Tile(object):
def __init__(self):
self.core = Button(window, height = 2, width = 3, bg = "#F4C364")
The root of the problem is that core is shared by all instances of the class by virtue of how you've defined it. You need to move creation of the button into the initializer.
I also suggest moving the configuration of the command into the button itself. The caller shouldn't need (nor care) how the button works internally. Personally I'd have the tile inherit from Button, but if you favor composition over inheritance I'll stick with that.
Example:
class tile(object):
def __init__(self):
self.core = Button(window, height = 2, width = 3, bg = "#F4C364"
command=self.cycle)
I have a rather simple goal of drawing a few spheres in a 3D space and adjusting their location according to some function. I want to use python with kivy to accomplish this because it make touch screen interfacing super simple, and I found a repository which takes care of most of the heavy lifting with respects to programming.
From this code in the main.py function, I want to draw n spheres, and then update their locations later on (this is done under the draw_elements(self) function, and LOP[] is a list of the class 'points')
def drawPoints():
print self.scene.objects
for i in range(len(self.LOP)):
PushMatrix()
point = self.LOP[i]
point.shape = self.scene.objects['Sphere']
point.color = _set_color(i/10., (i+1)/10., 0., id_color=(int(255/(1+i)), int(255/(1+i)), 255))
point.shape.scale = Scale((i+1)/10.0,(i+1)/10.0,(i+1)/10.0)
self.LOP[i] = point
point.shape.scale.origin = (point.loc[0],point.loc[1],point.loc[2])
_draw_element(point.shape)
PopMatrix()
drawPoints()
When the points are drawn, they are at their stated origin.
Later on the program calls the update_scene function thanks the the clock scheduler.
def update_scene(self, *largs):
def randLoc(point):
newLoc = (0.1*random.random(),0.1*random.random(),0.1*random.random())
oldLoc = point.shape.scale.origin
newLoc = ( newLoc[0]-0.05+oldLoc[0], newLoc[1]-0.05+oldLoc[1], newLoc[2]-0.05+oldLoc[2] )
return newLoc
def updateLocs(self):
for i in range(len(self.LOP)):
point = self.LOP[i]
point.shape.scale.origin = randLoc(point)
if not self.pause:
updateLocs(self)
pass
When this update function is run, only the sphere that was drawn last moves, though it does move correctly.
How can I move the other spheres I drew earlier?
(my source code can be found here though it's really just build off of the first repository)
Background: I have a code which generates the cartesian coordinates of a network of regular shapes (in this case triangles), and then plots the vertices of the shapes on a Tkinter Canvas as small circles. The process is automated and requires only height and width of the network to obtain a canvas output. Each vertex has the tags 'Vertex' and the vertex's number. Problem: I want to automatically connect the vertices of the shapes together (i.e dot to dot), I have looked into using find_closest and find_overlapping methods to do this, but as the network is composed of vertices at angles to one another, I often find find_overlapping to be unreliable (due to relying on a rectangular envelope), and find_closest appears limited to finding only one connection. As the vertices aren't necessarily connected in order, it is not possible to create a loop to simply connect vertex 1 --> vertex 2 etc. Question: Is there a way to efficiently get all of a vertex's neighbouring vertices and then 'connect the dots' without relying on individually creating lines between points using a manual method such as self.c.create_line(vertex_coord[1], vertex_coord[0], fill='black') for each connection? And would it be possible to share a small example of such a code? Thank you in advance for any help!Below is an abbreviated version of the canvas components of my code.Prototype Method:
from data_generator import *
run_coordinate_gen=data_generator.network_coordinates()
run_coordinate_gen.generator_go()
class Network_Canvas:
def __init__(self, canvas):
self.canvas=canvas
canvas.focus_set()
self.canvas.create_oval(Vertex_Position[0], dimensions[0], fill='black', tags=('Vertex1', Network_Tag, Vertex_Tag))
self.canvas.create_oval(Vertex_Position[5], dimensions[5], fill='black', tags=('Vertex2', Network_Tag, Vertex_Tag))
try:
self.canvas.create_line(Line_Position[5] ,Line_Position[0] , fill='black' tags=(Network_Tag,'Line1', Line_Tag )) #Connection Between 1 and 6 (6_1), Line 1
except:
pass
#Note: Line_Position, Dimensions and Vertex_Position are all lists composed of (x,y) cartesian coordinates in this case.
This is of course then replicated for each line and vertex throughout the network, but was only used for 90 vertices. The new version requires orders of magnitude more vertices and I am doing this with:
New Method:
#Import updated coordinate generator and run it as before
class Network_Canvas:
def __init__(self, canvas):
self.canvas=canvas
canvas.focus_set()
for V in range(len(vertex_coord_xy)):
self.canvas.create_text(vertex_coord_xy[V]+Text_Distance, text=V+1, fill='black', tags=(V, 'Text'), font=('Helvetica', '9'))
self.canvas.create_oval(vertex_coord_xy[V],vertex_coord_xy[V]+Diameter, fill='black', outline='black', tags=(V, 'Vertex'))
#loop to fit connections here (?)
I think any kind of nearest-neighbor search is going to be waay more time-intensive than just keeping track of the vertices, and there's no "automatic" connect-the-dots method that I can think of (plus, I don't see why such a method should be any faster than drawing them with create_line). Also, how will a nearest-neighbor search algorithm distinguish between the vertices of two separate, nearby (or overlapping) shapes if you aren't keeping track? Anyhow, in my opinion you've already got the right method; there are probably ways to optimize it.
I think that since your shapes are numerous, and there are complicated things you need to do with them, I would make a class for them, like the one I implemented below. It includes the "click to see neighboring vertices" functionality. All of the following code ran without errors. Image of the output shown below.
import Tkinter as TK
import tkMessageBox
# [Credit goes to #NadiaAlramli](http://stackoverflow.com/a/1625023/1460057) for the grouping code
def group(seq, groupSize):
return zip(*(iter(seq),) * groupSize)
Network_Tag, Vertex_Tag, Line_Tag = "network", "vertex", "line"
class Shape:
def __init__(self, canvas, vertexCoords, vertexDiam):
self.vertexIDs = []
self.perimeterID = None
self.vertexCoords = vertexCoords
self.vertexRadius = vertexDiam/2
self.canvas = canvas
def deleteVertices(self):
for ID in self.vertexIDs:
self.canvas.delete(ID)
self.vertexIDs = []
def bindClickToVertices(self):
coordsGrouped = group(self.vertexCoords, 2)
num = len(coordsGrouped)
for k in range(len(self.vertexIDs)):
others = [coordsGrouped[(k-1)%num], coordsGrouped[(k+1)%num]]
self.canvas.tag_bind(self.vertexIDs[k], '<Button-1>',
lambda *args:tkMessageBox.showinfo("Vertex Click", "Neighboring vertices: "+str(others)))
def drawVertices(self):
for x, y in group(self.vertexCoords, 2):
self.vertexIDs.append(self.canvas.create_oval(x-self.vertexRadius, y-self.vertexRadius, x+self.vertexRadius, y+self.vertexRadius, fill='black', tags=(Network_Tag, Vertex_Tag)))
self.bindClickToVertices()
def updateVertices(self):
self.deleteVertices()
self.drawVertices()
def deletePerimeter(self):
if self.perimeterID is not None:
self.canvas.delete(self.perimeterID)
self.perimeterID = None
def drawPerimeter(self):
print "creating line:", (self.vertexCoords + self.vertexCoords[0:2])
self.perimeterID = self.canvas.create_line(*(self.vertexCoords + self.vertexCoords[0:2]), fill='black', tags=(Network_Tag, Line_Tag))
def updatePerimeter(self):
self.deletePerimeter()
self.drawPerimeter()
def deleteShape(self):
self.deleteVertices()
self.deletePerimeter()
def updateShape(self):
self.updateVertices()
self.updatePerimeter()
It can be used very simply, like this:
root = TK.Tk()
frame = TK.Frame(root)
canvas = TK.Canvas(frame, width=1000, height=1000)
frame.grid()
canvas.grid()
# create a bunch of isoceles triangles in different places:
shapes = []
for dx, dy in zip(range(0,1000, 30), range(0,1000, 30)):
shapes.append(Shape(canvas, [0+dx, 0+dy, 10+dx, 10+dy, 20+dx, 0+dy], 5))
# draw (or redraw) the shapes:
for shape in shapes:
shape.updateShape()
# move one of the shapes and change it to a square
shapes[10].vertexCoords = [50, 10, 60, 10, 60, 20, 50, 20]
shapes[10].updateShape()
# delete all the odd-numbered shapes, just for fun:
for k in range(len(shapes)):
if k%2 == 1:
shape.deleteShape()
root.mainloop()
Output: