generating animation in tkinter - python

I wrote a small program using tkinter to generate a figure based on parameters I'm giving the program (through gui interface). I am drawing the figure directly with tkinter using Canvas. Now I want to generate a series of figures and put them in animated loop. I can generate a gif animation and load it into the figure but the question is if I can do this directly in tkinter. I can put everything in loop, but then I'll lose the event handler. Any other option?
Edit
Here is a simple script:
#!/usr/bin/python
from Tkinter import *
import ttk
import numpy as np
colors = {1:'#F00',
2:'#0F0',
3:'#00F',
4:'#FF0',
5:'#0FF'}
root = Tk()
canvas = Canvas(root,width=400,height=400)
label = Label(root,text='seed')
values = StringVar()
combo = ttk.Combobox(root)
def painter(event):
seed = int(combo.get())
np.random.seed(seed)
nrows = 10
ncols = 10
Y = 0
dx = 400/nrows
dy = 400/ncols
for i in range(nrows):
X = 0
for j in range(ncols):
n = np.random.randint(1,5)
col = colors[n]
canvas.create_rectangle(X,Y,X+dx,Y+dy,width=1,fill=col)
X += dx
Y += dy
canvas.grid(column=0,row=0,rowspan=2)
combo.grid(row=1,column=1)
label.grid(row=0,column=1)
combo['values'] = [1,2,3,4]
combo.bind('<<ComboboxSelected>>',painter)
combo.current(0)
painter(None)
root.mainloop()
I could change the painter function to include an infinite loop but this way I never reach the mainloop(), so I need to call this only after the main loop is created, but how?

a simple example of some animation using tkinter canvas:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("Canvas animation example")
self.c = tk.Canvas(self, width=400, height=400)
self.c.pack()
self.f_index = 0 # index so we know which frame to draw next
# array to hold our frame data,
# you'll probably need this to hold more than
# just a set of coordinates to draw a line...
self.f_data = []
for num in range(0, 400, 5): # make up a set of fake data
self.f_data.append([num, num, num+10, num+10])
def next_frame(self):
data = self.f_data[self.f_index] # fetch frame data
self.c.delete('all') # clear canvas
self.c.create_line(*data) # draw new frame data
self.f_index += 1 # increment frame index
if (self.f_index >= len(self.f_data)): # check and wrap if at end of sequence
self.f_index = 0
self.c.after(50, self.next_frame) # call again after 50ms
if __name__ == "__main__":
app = App()
app.next_frame() # called manually once to start animation
# could be started with 'after' instead if desired
app.mainloop()
note that the more items you want to delete and redraw the slower this code will perform. you need to have realistic expectations, drawing a few 10's of items will be fine, a few 100's might be ok on a decent pc, but 1000's of items would be very poorly performing.

Related

Tkinter widgets created in an Update function run by tk.after function do not create untill the aforementioned Update ends

I intend to make a Py code which creates a tkinter dot that turns on a key press and deletes on a key press of couple keys.
The dot already is functional but i need it switch on and off on certain keypresses/mouse clicks which means i need an outside tkinter.mainloop() Update function.
The Update function with a while in it to constantly check if conditions to turn it off/on are present. But the Tkinter widget Somehow gets applied to the screen Only when the function nds. Like widget could be created but it will only take effect when function ends. And i need to turn it off/on dynamically.
I have tried to use a tkinter.after() with additional one at the end of called function only to find out an error of Recursion depth. What i expected to happen was that the function would be called over and over again, instead it runs that function like a while loop. I also have tried Asyncio.run() but it would result not making it visible till the function ends at least once. And I need to change it dynamically.
from tkinter import *
from tkinter import Canvas
from winsound import Beep
from time import sleep
import asyncio
import keyboard
import mouse
root = Tk()
width = root.winfo_screenwidth()
height = root.winfo_screenheight()
class tk_Dot():
def __init__(self,x=-1,y=-1,radius=4,color="red"):
self.x = x
if x == -1:
self.x = width/2-radius//2
print(self.x)
self.y = y
if y == -1:
self.y = height/2+radius//2
print(self.y)
self.radius=radius
self.color = color
self.lines = []
self.count = 1
def line(self,i):
return canvas.create_line(self.x, self.y-i, self.x+self.radius, self.y-i, fill=self.color)
def create(self):
self.lines = []
for i in range(0,self.radius):
self.lines.append(self.line(i))
def delete(self):
for i in range(0,self.radius):
canvas.delete(self.lines[i])
canvas.dtag(self.lines[i])
opacity_of_tk_window = 1 # From 0 to 1 0 meaning completely transparent 1 meaning everything created in canvas will give the color it was given
root.attributes('-alpha',opacity_of_tk_window)
# Invisible Tkinter window label
root.overrideredirect(True)
# Makes Transparent background
transparent_color = '#f0f0f0'
root.wm_attributes('-transparent', transparent_color)
canvas = Canvas()
# Rectangle filled with color that is specified above as the transparent color so practically making transparent background
canvas.create_rectangle(0, 0, width, height, fill=transparent_color)
canvas.pack(fill=BOTH, expand=1)
radius = 2
radius = 1+radius\*2
# Create a dot class
game_dot = tk_Dot(width/2-radius//2+1,height/2+1+radius//2,radius,"Red")
# Create a Dot at the middle of the calorant crosshair
# game_dot.create()
# Delete the dot
# game_dot.delete()
def Update():
game_dot.create()
print("Dot should be visible by now")
print("Is it?")
sleep(5) #sec
print("Oh yeah after the function ends.") # the problem
def Delete():
game_dot.delete()
root.geometry('%dx%d+%d+%d' % (width, height, -2,-2))
# Tkinter window always on top
root.attributes('-topmost',True)
root.after(1000,Update())
root.mainloop()

pyqtgraph scrolling plots: plot in chunks, show only latest 10s samples in current window

I have trouble using pygtgraph scrolling plots
Expected Results
The expected results are quite similar to the pyqtgraph-examples-scrolling plots-plot5
X-values are times, which can be generated by a simple function. Y-Values are random values.
Each 10 seconds samples as one chunk and each plot can have max. 30 seconds samples, which means 3 chunks. The current plot window only shows the latest 10 seconds samples
For example, now there are total 60 seconds samples:
Data between 50s-60s will be viewed in the current window
Data between 30s-50s could be viewed by using the mouse to drag the x-axis backward
Data between 0-30s will not be displayed
My Code
My current code is below, it can only show latest 30s data.
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import random
win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('Scrolling Plots')
p1 = win.addPlot()
p1.setYRange(0,10)
xVal = [0]
yVal = [0]
def genTime(): # used to generate time
t = 0
while True:
t += np.random.random_sample()
yield t
t = np.ceil(t)
xTime = genTime()
#=====================================================
viewSize = 10 # current window show only latest 10s data
plotSize = 30 # plot 30s data -> 3 chunk
lstCurves = [] # List for Curves
def update():
global p1, xVal, yVal, lstCurves
#for c in lstCurves:
# c.setPos(xVal[-1], 0)
i = np.ceil(xVal[-1]) % viewSize # e.g. when time is 9.2s -> one 10s view size is full, append to curves list as one chunk
if i == 0:
curve = p1.plot()
lstCurves.append(curve)
xValLast = xVal[-1]
yValLast = yVal[-1]
xVal = [xValLast]
yVal = [yValLast]
while len(lstCurves) > 3: # max 3 chunk (30 s)
p1.removeItem(lstCurves.pop(0)) # remove the oldest 10s
else:
curve = lstCurves[-1] # latest 10s curve
xVal.append(next(xTime))
yVal.append(random.randint(0,9))
curve.setData(xVal, yVal)
print(len(lstCurves))
#======================================================
timer = pg.QtCore.QTimer()
timer.timeout.connect(update)
timer.start(1000)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
Problem
I have tried using curve.setPos(xx, 0), It looks like the whole curve is moving along the x-axis, but the mapping relationship between X-value and Y-value is broken
I have also tried using setXRange() to dynamically change x-axis display-range in update() func. But in this case, I can't use the mouse to drag the x-axis back to view the old data any more.
My English is not good, I hope you can understand my question. Any suggestions would be sincerely appreciated!
Problem
The reasons your code don't do what you want are:
When you drag to view the other chunks, you disable the automatic auto-range of the plot, after that, you will have to manually drag the plot every time you want to see the new data. Also, by default, the auto-range of the plot will cover all the data that you are plotting.
When you use the setRange() method inside the update function, it will force that range every time you add another value to the data. Then the drag will not work as you want
What can you do
Well, from my perspective using the mouse drag to visualize the other data is not very convenient, I suggest to use an external widget to control the range of the data you want to view, like, a slider, scroll bar, spin box, ...
A QScrollBar can do the job and it will look esthetic in a GUI.
Before my Alternative Solution, I have a suggestion for you:
Use objects to create your widget, generate a class and use the variables as attributes, with this you avoid the use of the keyword global, and you could reuse the widget for other purposes.
Alternative Solution
Try this:
import sys
import random
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
class MyApp(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
## Creating the Widgets and Layouts
self.plot_widget = pg.PlotWidget()
self.layout = QtGui.QVBoxLayout()
self.sbutton = QtGui.QPushButton("Start / Continue")
self.ebutton = QtGui.QPushButton("Stop")
self.timer = pg.QtCore.QTimer()
self.scroll = QtGui.QScrollBar(QtCore.Qt.Horizontal)
## Creating the variables and constants
self.data = [[0], [random.randint(0,9)]] ## [xVal, yVal] to create less variables
self.plot_item = self.plot_widget.plot(*self.data)
self.plot_widget.setYRange(0, 10)
self.xTime = self.genTime()
self.vsize = 10
self.psize = 30
## Building the Widget
self.setLayout(self.layout)
self.layout.addWidget(self.sbutton)
self.layout.addWidget(self.ebutton)
self.layout.addWidget(self.plot_widget)
self.layout.addWidget(self.scroll)
## Changing some properties of the widgets
self.plot_widget.setMouseEnabled(x=False, y=False)
self.ebutton.setEnabled(False)
self.scroll.setEnabled(False)
self.scroll.setMaximum(self.psize-self.vsize)
self.scroll.setValue(self.psize-self.vsize)
## Coneccting the signals
self.sbutton.clicked.connect(self.start)
self.ebutton.clicked.connect(self.stop)
self.timer.timeout.connect(self.update)
self.scroll.valueChanged.connect(self.upd_scroll)
def genTime(self): # used to generate time
t = 0
while True:
t += np.random.random_sample()
yield t
t = np.ceil(t)
def upd_scroll(self):
val = self.scroll.value()
xmax = np.ceil(self.data[0][-1+self.vsize-self.psize+val])-1
xmin = xmax-self.vsize
self.plot_widget.setXRange(xmin, xmax)
def update(self):
num = len(self.data[0])
if num <= self.psize:
self.plot_item.setData(*self.data)
else:
self.plot_item.setData(
self.data[0][-self.psize:],
self.data[1][-self.psize:]
)
if num == self.vsize:
self.scroll.setEnabled(True)
self.data[0].append(next(self.xTime))
self.data[1].append(random.randint(0,9))
if num > self.vsize :
self.upd_scroll()
def start(self):
self.sbutton.setEnabled(False)
self.ebutton.setEnabled(True)
self.timer.start(100)
def stop(self):
self.sbutton.setEnabled(True)
self.ebutton.setEnabled(False)
self.timer.stop()
self.upd_scroll()
def closeEvent(self, event):
self.timer.stop()
event.accept()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())
It may look like this:

Setting a background image within a set Tkinter animation framework

I am currently working on a fruit ninja project for a class. Everything functionally works fine, however, when I try to put in a background image for the game runs extremely slow. In order for the game to look polished, I need everything to work smoothly while having the background of the game show. Other solutions I have come across and have tried to understand simply do not work or the file never ends up running.
FYI: I am working in python 2.7.
I have tried some other suggestions for adding a background, such as using a label function, however, when I try to implement it I get a variety of errors and it just does not seem to work in my animation framework.
def run(width=300, height=300):
def redrawAllWrapper(canvas, data):
canvas.delete(ALL)
canvas.create_rectangle(0, 0, data.width, data.height,
fill='white', width=0)
redrawAll(canvas, data)
canvas.update()
def mousePressedWrapper(event, canvas, data):
mousePressed(event, data)
redrawAllWrapper(canvas, data)
def keyPressedWrapper(event, canvas, data):
keyPressed(event, data)
redrawAllWrapper(canvas, data)
def timerFiredWrapper(canvas, data):
timerFired(data)
redrawAllWrapper(canvas, data)
# pause, then call timerFired again
canvas.after(data.timerDelay, timerFiredWrapper, canvas, data)
# Set up data and call init
class Struct(object): pass
data = Struct()
data.width = width
data.height = height
data.timerDelay = 10 # milliseconds
root = Tk()
root.resizable(width=False, height=False) # prevents resizing window
init(data)
# create the root and the canvas
canvas = Canvas(root, width=data.width, height=data.height)
canvas.configure(bd=0, highlightthickness=0)
canvas.pack()
# set up events
root.bind("<Button-1>", lambda event:
mousePressedWrapper(event, canvas, data))
root.bind("<Key>", lambda event:
keyPressedWrapper(event, canvas, data))
timerFiredWrapper(canvas, data)
# and launch the app
root.mainloop() # blocks until window is closed
print("bye!")
run(1200, 700)
with my current framework, I write all the necessary code within the init, timerFired, redrawAll, keyPressed, and mousePressed functions above this run function.
With my current implementation of the background. I use PhotoImage on a 1200 x 700 gif file and draw the image across the whole screen in the redrawAll function (which is called every 10 milliseconds). Without drawing this one image, my game runs very smoothly, however, upon drawing the image in redrawAll, the game lags significantly, so I do know the source of the lag is drawing the background image.
Here is the line of code that draws it in redrawAll:
canvas.create_image(data.width//2, data.height//2, image = data.background)
Is this only because I do it in redrawAll which continuously draws the image every time the function is called making it slow? Is simply having an image that large in Tkinter making it slow? What is the source?
This there a way to simply draw the image once on the background and have it never change? Or is there any way to not have lag? I just find it odd. Again, this is in python 2.7 on a Mac.
Thanks!
You don't have to remove and add again all elements to refresh screen. You can move elements and canvas will draw it correctly
This code create 1000 small rectangles and move them randomly on background.
Tested with Python 3.7 but on 2.7 should work too.
With 5_000 rectangles it slows down but it still works good (but not perfect). With 10_000 it slows down too much.
from Tkinter import *
from PIL import Image, ImageTk
import random
IMAGE_PATH = 'background.jpg'
class Struct(object):
pass
def run(width=300, height=300):
def init(data):
# create 1000 rectangles in random position
for _ in range(1000):
x = random.randint(0, data.width)
y = random.randint(0, data.height)
data.data.append(canvas.create_rectangle(x, y, x+10, y+10, fill='red'))
def mousePressedWrapper(event, canvas, data):
#mousePressed(event, data)
pass
def keyPressedWrapper(event, canvas, data):
#keyPressed(event, data)
pass
def timerFiredWrapper(canvas, data):
# move objects
for rect_id in data.data:
x = random.randint(-10, 10)
y = random.randint(-10, 10)
canvas.move(rect_id, x, y)
# pause, then call timerFired again
canvas.after(data.timerDelay, timerFiredWrapper, canvas, data)
# Set up data and call init
data = Struct()
data.width = width
data.height = height
data.timerDelay = 10 # milliseconds
data.data = [] # place for small red rectangles
root = Tk()
root.resizable(width=False, height=False) # prevents resizing window
# create the root and the canvas
canvas = Canvas(root, width=data.width, height=data.height)
canvas.configure(bd=0, highlightthickness=0)
canvas.pack()
#canvas.create_rectangle(0, 0, data.width, data.height, fill='white', width=0)
img = Image.open(IMAGE_PATH)
img = img.resize((data.width, data.height))
photo = ImageTk.PhotoImage(img)
canvas.create_image(0, 0, image=photo, anchor='nw')
init(data) # init after creating canvas because it create rectangles on canvas
# set up events
root.bind("<Button-1>", lambda event:
mousePressedWrapper(event, canvas, data))
root.bind("<Key>", lambda event:
keyPressedWrapper(event, canvas, data))
timerFiredWrapper(canvas, data)
# and launch the app
root.mainloop() # blocks until window is closed
print("bye!")
run(1200, 700)

Tkinter canvas move leaves a pixel trail

I'm working on a game in a Tkinter canvas where points move around the screen. I place each point at a location with tkinter.Canvas.create_oval(...) and subsequently move the points with tkinter.Canvas.move(pointID,delta_x,delta_y).
My problem is that the points seem to leave a trail behind when they are moved. I made a simplified example that demonstrates my problem.
from tkinter import Canvas,mainloop,Tk
import numpy as np
import random
import traceback
import threading
import time
from queue import Queue
class Point:
def __init__(self,the_canvas,uID):
self.uID = uID
self.location = np.ones((2)) * 200
self.color = "#"+"".join([random.choice('0123456789ABCDEF') for j in range(6)])
self.the_canvas = the_canvas
self.the_canvas.create_oval(200,200,200,200,
fill=self.color,outline=self.color,width=6,
tags=('runner'+str(self.uID),'runner'))
def move(self):
delta = (np.random.random((2))-.5)*20
self.the_canvas.move('runner'+str(self.uID),delta[0],delta[1])
def queue_func():
while True:
time.sleep(.25)
try:
next_action = the_queue.get(False)
next_action()
except Exception as e:
if str(e) != "":
print(traceback.format_exc())
the_queue = Queue()
the_thread = threading.Thread(target=queue_func)
the_thread.daemon = True
the_thread.start()
window = Tk()
window.geometry('400x400')
the_canvas = Canvas(window,width=400,height=400,background='black')
the_canvas.grid(row=0,column=0)
points = {}
for i in range(100):
points[i] = Point(the_canvas,i)
def random_movement():
while True:
for point in points.values():
point.move()
the_queue.put(random_movement)
mainloop()
And the result is something like this:
I need to be able to move Points around cleanly, without leaving anything behind.
I tried changing the move() function so that each point is deleted according to its tag and redrawn at the new location, but that results in the same problem.
I have tried fill='' and also outline='' in the Canvas.oval config, but this does not help.
The behavior of these pixel trials seem erratic, like they will disappear over time, only leaving a limited amount of footprints behind.
I have tried removing the time.sleep(.2) from the movement loop, and that seems to make the problem a lot more pronounced.
I've found that the only way to cleanup these rogue colored pixels is to run canvas.delete("all"), so as of now, my only solution is to delete everything and redraw everything constantly. That doesn't seem like a great solution to me.
What's a good way to avoid these "pixel trails"? It really just seems like a bug to me, but maybe I'm making a blunder somewhere.
After some digging I found this post here: Python3 tkinter.Canvas.move() method makes artifacts on screen
The problem there was the borders of the oval. So what I did was remove the borders and make the oval slightly larger to compensate and it looks like that did the trick.
If you change this line:
self.the_canvas.create_oval(200, 200, 200, 200,
fill=self.color, outline=self.color, width=6,
tags=('runner' + str(self.uID), 'runner'))
To this:
self.the_canvas.create_oval(200,200,206,206,
fill=self.color,outline='', width=0,
tags=('runner'+str(self.uID),'runner'))
the problem should go away with or without threading.
If you would like to see what your code would look like without threading here is an example:
import tkinter as tk
import numpy as np
import random
class Point:
def __init__(self, the_canvas, uID):
self.uID = uID
self.location = np.ones((2)) * 200
self.color = "#"+"".join([random.choice('0123456789ABCDEF') for j in range(6)])
self.the_canvas = the_canvas
self.the_canvas.create_oval(200, 200, 206, 206,
fill=self.color, outline='', width=0,
tags=('runner'+str(self.uID), 'runner'))
def move(self):
delta = (np.random.random((2))-.5)*20
self.the_canvas.move('runner'+str(self.uID), delta[0], delta[1])
window = tk.Tk()
window.geometry('400x400')
the_canvas = tk.Canvas(window, width=400, height=400, background='black')
the_canvas.grid(row=0, column=0)
points = {}
for i in range(100):
points[i] = Point(the_canvas, i)
def random_movement():
for point in points.values():
point.move()
window.after(50, random_movement)
random_movement()
window.mainloop()
Results:
Weirdly enough the issue seems to be coming from
width=6 in create_oval()
getting rid of it seems to resolve the issue.
The way you've originally setup your create_oval() is making an oval of zero area (i.e. non-existent) and border width of 6... which is obviously a troubelsom combination.
So here's the new code, with some extra modfications:
from tkinter import Canvas,mainloop,Tk
import numpy as np
import random
import traceback
import time
class Point:
def __init__(self,uID):
self.uID = uID
self.location = np.ones((2)) * 200
self.color = "#" + "".join([random.choice('0123456789ABCDEF') for j in range(6)])
def move(self):
delta = (np.random.random((2)) - .5) * 20
self.location += delta
def draw(self,canv):
x0, y0 = self.location
canv.create_oval(
x0, y0, x0 + 2, y0 + 2,
fill=self.color, outline=self.color,
tags=('runner'+str(self.uID),'runner')
)
def repeater(window):
the_canvas.delete('all')
for i in points:
i.move()
i.draw(the_canvas)
window.after(25, repeater, window)
window = Tk()
window.geometry('400x400')
the_canvas = Canvas(window,width=400,height=400,background='black')
the_canvas.grid(row=0,column=0)
points = []
for i in range(100):
points.append(Point(i))
repeater(window)
window.mainloop()
Also, btw, deleting all elements on the canvas is the way to clear it. it's not wasteful since you're updating all elements on it anyway.

Tkinter: configure multiple widgets simultaneously

I want to set the background of multiple widgets in a row at once. However, in the example you can see that sometimes the colorization is ugly: Widgets change color in separate times. How to update the appearance (configuration) of multiple widgets simultaneously? Virtual events would solve this issue?
import tkinter
class Application(tkinter.Tk):
TITLE = 'Simultaneous Widget Blink Test'
BLINK_INT = 75
def __init__(self, rows=4, cols=10):
super().__init__()
self.rows, self.cols = rows, cols
self.frames = [[] for _ in range(rows)]
self.labels = [[] for _ in range(rows)]
self.withdraw() # assembling in background...
self.resizable(width=False, height=False)
self.title(self.TITLE)
self.build()
for ri in range(self.rows):
self.curr_ri = ri
self.set_row_style()
self.curr_ri, self.curr_n = 0, 0
self.deiconify() # showing it
self.blink_again()
def build(self):
for ri in range(self.rows):
for ci in range(self.cols):
f = tkinter.Frame(self, width=40, height=40)
self.frames[ri].append(f)
f.grid(row=ri, column=ci, sticky='news')
l = tkinter.Label(f, text='{}{}'.format(ri, ci))
self.labels[ri].append(l)
l.place(relx=0.5, rely=0.5, anchor='c')
def blink_again(self):
if self.curr_n == 10:
self.curr_n = 0
self.curr_ri += 1
if self.curr_ri == self.rows:
self.curr_ri = 0
self.curr_n += 1
self.highlight_row()
def highlight_row(self):
self.set_row_style(bg='orange')
self.after(self.BLINK_INT, self.restore_row)
def restore_row(self):
self.set_row_style()
self.after(self.BLINK_INT, self.blink_again)
def set_row_style(self, bg='white'):
for w in self.frames[self.curr_ri]:
w.configure(bg=bg)
for w in self.labels[self.curr_ri]:
w.configure(bg=bg)
if __name__ == '__main__':
app = Application()
app.mainloop()
Thanks in advance!
EDIT: More simplified example code.
The way you are updating the widgets is the proper way to modify multiple widgets at once. As long as you don't have any calls to update or update_idletask in the loop that modifies all of the widgets, the modifications should all appear simultaneously the next time there is a refresh on the screen (ie: when your function returns control to the event loop).
When I run your code I see no flickering. It all behaves as I expect.

Categories