how to run tkinter gif loop only ONCE - python

this is the code that i used to load a gif into a label object in tkinter
class ImageLabel(tk.Label):
"""a label that displays images, and plays them if they are gifs"""
def load(self, im):
if isinstance(im, str):
im = Image.open(im)
print(im.is_animated)
print(im.n_frames)
self.loc = 0
self.frames = []
try:
for i in count(1):
self.frames.append(ImageTk.PhotoImage(im.copy()))
im.seek(i)
except EOFError:
pass
try:
self.delay = im.info['duration']
except:
self.delay = 900
if len(self.frames) == 1:
self.config(image=self.frames[0])
else:
self.next_frame()
def unload(self):
self.config(image="")
self.frames = None
def next_frame(self):
if self.frames:
self.loc += 1
self.loc %= len(self.frames)
self.config(image=self.frames[self.loc])
self.after(self.delay, self.next_frame)
my aim is to load the gif in only a single loop based on the number of frames like lets say there are 5 frames in an image it only loops through that and stops
can someone help me with this.
if i change the
for i in count(im.n_frames):
it only loads the first frame and stops after that.

there are two things that would be required to make this work in this code snippet
Number 1 change the loc intially to -1
secondly change the next_frame function to
def next_frame(self):
if self.frames:
self.loc += 1
self.config(image=self.frames[self.loc])
self.after(self.delay, self.next_frame)

This is line for line an answer provided as to how to get tkinter to loop a gif indefinitely (except that you changed the duration of the delay).
I'm not sure you realize what count is doing here. Count, imported from itertools, is going to infintely count (acting as a "while(true)" but incrementing a number) unless given a barrier. It accepts two parameters (start= , stop= ) but if only given one, it defaults to start. So you have initiated a count at the value of im.n_frames.
What's happening is that you are loading the first frame, and starting the count at the last frame. When it then goes to find the next frame, you're hitting EOF, and starting the whole thing over again.
If the images are indexed starting at 1, try
for i in range(1, im.n_frames+1):

If you want to play the animation of GIF image only once, you need to modify next_frame() not to call .after() when the last frame has been shown.
Below is the modified ImageLabel class:
class ImageLabel(tk.Label):
"""a label that displays images, and plays them if they are gifs"""
def load(self, im):
if isinstance(im, str):
im = Image.open(im)
print(im.is_animated)
print(im.n_frames)
self.delay = im.info.get('duration', 900)
# load all the frames inside the image
self.frames = []
for i in range(im.n_frames):
im.seek(i)
self.frames.append(ImageTk.PhotoImage(im.copy()))
# start the animation
self.next_frame()
def unload(self):
self.config(image="")
self.frames = None
# modified to play the animation only once
def next_frame(self, loc=0):
self.config(image=self.frames[loc])
if loc < len(self.frames)-1:
self.after(self.delay, self.next_frame, loc+1)

Related

Choosing from a List of methods in a tkinter Button

Good Day,
I'm new to this forum (and quite new to programming), so I hope my question is properly formulated.
I've been trying to create a GUI in python using tkinter, and I want to have two buttons calling methods of two different classes. One method is defining an integer, the second one is reporting content. I'd have a list of objects of the latter class, and I want to choose the right instance by the integer. Here's a MWE:
import tkinter as tk
class data:
def __init__(self, content):
self.content = content
def report(self):
print("This is reported as self.content:" + str(self.content)) #This doesnt report the correct value for some reason?
print("The Class does register the correct idx:" + str(Selector.idx))
print("Using the Dict the correct value can be returned:" + str(vocables[Selector.idx].content))
class increment:
def __init__(self):
self.idx = 0
def increase(self):
self.idx += 1
print(self.idx)
vocables[self.idx].report()
root = tk.Tk()
Selector = increment()
vocables = []
for id in range(10):
vocables.append(data(id))
# print(vocables[id].content)
CheckVocable = tk.Button(root, text="Report", command=vocables[Selector.idx].report)
CheckVocable.pack()
NextVocable = tk.Button(root, text="Increase Index", command=Selector.increase)
NextVocable.pack()
root.mainloop()
I do not understand why the print of line 8 always reports the value of the first item in the list (vocabules[0] in this instance) instead of my desired value, which is returned in all other print cases. Am I messing up the work with classes or is the button behavior confusing me?
Thanks in advance!

Tkinter - memory leak with canvas

I have a Python script that handles Modbus communications. One feature I added was a "graph" that shows the response times along with a color coded line that indicates if the response was successful, had an exception, or an error. The graph is just a scrollable canvas widget from Tkinter.
After graphing a certain number of lines old lines will be deleted and then a new one will be added to the end. For this example I have it set to 10, which means there will never be more than 10 lines on the canvas at once.
The code works correctly but there is a memory leak somewhere in this function. I let it run for about 24 hours and it took about 6x more memory after 24 hours. The function is part of a larger class.
My current guess is that my code causes the canvas size to constantly "expand," which slowly eats up the memory.
self.lineList = []
self.xPos = 0
def UpdateResponseTimeGraph(self):
if not self.graphQueue.empty():
temp = self.graphQueue.get() #pull from queue. A separate thread handles calculating the length and color of the line.
self.graphQueue.task_done()
lineName = temp[0] #assign queue values to variables
lineLength = temp[1]
lineColor = temp[2]
if len(self.lineList) >= 10: #if more than 10 lines are on the graph, delete the first one.
self.responseTimeCanvas.delete(self.lineList[0])
del self.lineList[0]
#Add line to canvas and a list so it can be referenced.
self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-lineLength,
fill=lineColor, outline=''))
self.xPos += 5 #will cause the next line to start 5 pixels later. MEMORY LEAK HERE?
self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))
self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.
self.graphFrame.after(10, self.UpdateResponseTimeGraph)
One solution could be loop back to the start of the graph once a limit is reached but I would rather not do this since it may be confusing where the graph starts. Usually I have far more responses than 10.
EDIT:
I'm still doing to trail and error stuff but it looks like the memory leak can be eliminated with Bryan's suggestion as long as the line attributes are not changed via itemconfig. The code below should be able to run as is, if you're on python 2.7 change the import statement from tkinter to Tkinter (lower case vs uppercase t). This code will have the memory leak in it. Comment out the itemconfig line and it will be eliminated.
import tkinter
from tkinter import Tk, Frame, Canvas, ALL
import random
def RGB(r, g, b):
return '#{:02x}{:02x}{:02x}'.format(r, g, b)
class MainUI:
def __init__(self, master):
self.master = master
self.lineList = []
self.xPos = 0
self.maxLine = 122
self.responseIndex = 0
self.responseWidth = 100
self.responseTimeCanvas = Canvas(self.master, height=self.responseWidth)
self.responseTimeCanvas.pack()
self.UpdateResponseTimeGraph()
def UpdateResponseTimeGraph(self):
self.lineLength = random.randint(10,99)
if len(self.lineList) >= self.maxLine:
self.lineLength = random.randint(5,95)
self.responseTimeCanvas.coords(self.lineList[self.responseIndex % self.maxLine], self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength)
#if i comment out the line below the memory leak goes away.
self.responseTimeCanvas.itemconfig(self.lineList[self.responseIndex % self.maxLine], fill=RGB(random.randint(0,255), random.randint(0,255), random.randint(0,255)))
else:
self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength,
fill=RGB(random.randint(0,255), random.randint(0,255), random.randint(0,255)), outline=''))
self.xPos += 5 #will cause the next line to start 5 pixels later. MEMORY LEAK HERE?
self.responseIndex += 1
self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))
self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.
self.responseTimeCanvas.after(10, self.UpdateResponseTimeGraph)
mw = Tk()
mainUI = MainUI(mw)
mw.mainloop()
The underlying tk canvas doesn't reuse or recycle object identifiers. Whenever you create a new object, a new identifier is generated. The memory of these objects is never reclaimed.
Note: this is memory inside the embedded tcl interpreter, rather than memory managed by python.
The solution is to reconfigure old, no longer used elements rather than deleting them and creating new ones.
Here's the code with no memory leak. The original source of the leak was me deleting the old line then creating a new one. This solution moves the first the line to the end then change's its attributes as necessary. I had a second 'leak' in my example code where I was picking a random color each time which lead to the number of colors used eating up a lot of memory. This code just prints green lines but the length will be random.
import tkinter
from tkinter import Tk, Frame, Canvas, ALL
import random
def RGB(r, g, b):
return '#{:02x}{:02x}{:02x}'.format(r, g, b)
class MainUI:
def __init__(self, master):
self.master = master
self.lineList = []
self.xPos = 0
self.maxLine = 122
self.responseIndex = 0
self.responseWidth = 100
self.responseTimeCanvas = Canvas(self.master, height=self.responseWidth)
self.responseTimeCanvas.pack()
self.UpdateResponseTimeGraph()
def UpdateResponseTimeGraph(self):
self.lineLength = random.randint(10,99)
if len(self.lineList) >= self.maxLine:
self.lineLength = random.randint(5,95)
self.responseTimeCanvas.coords(self.lineList[self.responseIndex % self.maxLine], self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength)
self.responseTimeCanvas.itemconfig(self.lineList[self.responseIndex % self.maxLine], fill=RGB(100, 255, 100))
else:
self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength,
fill=RGB(100, 255, 100), outline=''))
self.xPos += 5 #will cause the next line to start 5 pixels later.
self.responseIndex += 1
self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))
self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.
self.responseTimeCanvas.after(10, self.UpdateResponseTimeGraph)
mw = Tk()
mainUI = MainUI(mw)
mw.mainloop()

Passing variable from one function to another in same class?

I'm trying to create a button that changes colors on click.
After digging around in an old Python book I haven't looked at in years, I've gotten to the point where I make the the button, but I can't figure out how to pass i into the second function so it increments and is then is reset to 0.
I suppose I could just increment I on click in the first function, but now I'm annoyed and want to figure it out.
Instead of self.change_color I tried change_color(i). That threw an error. Same with trying self.change_color(i).
I'm not sure what to do at this point.
import tkinter
class joeGUI:
def __init__(self):
i = 0
colorArray = ['blue','DarkGreen','red','yellow']
self.main_window = tkinter.Tk()
self.color_button = tkinter.Button(self.main_window,
text = 'Click to Change Color',
command = self.change_color,
bg = colorArray[i])
self.color_button.pack()
tkinter.mainloop()
def change_color(self):
if (count < 3):
count += 1
else:
count = 0
return count;
joe_gui = joeGUI()
Store i as a class attribute (self.i = 0) and change the references of count to self.i.

Undo and Redo in an Tkinter Entry widget?

Is there a way to add undo and redo capabilities in Tkinter Entry widgets or must I use single line Text widgets for this type of functionality?
If the latter, are there any tips I should follow when configuring a Text widget to act as an Entry widget?
Some features that might need tweaking include trapping the Return KeyPress, converting tab keypresses into a request to change focus, and removing newlines from text being pasted from the clipboard.
Check the Tkinter Custom Entry. I have added Cut, Copy, Paste context menu, and undo redo functions.
# -*- coding: utf-8 -*-
from tkinter import *
class CEntry(Entry):
def __init__(self, parent, *args, **kwargs):
Entry.__init__(self, parent, *args, **kwargs)
self.changes = [""]
self.steps = int()
self.context_menu = Menu(self, tearoff=0)
self.context_menu.add_command(label="Cut")
self.context_menu.add_command(label="Copy")
self.context_menu.add_command(label="Paste")
self.bind("<Button-3>", self.popup)
self.bind("<Control-z>", self.undo)
self.bind("<Control-y>", self.redo)
self.bind("<Key>", self.add_changes)
def popup(self, event):
self.context_menu.post(event.x_root, event.y_root)
self.context_menu.entryconfigure("Cut", command=lambda: self.event_generate("<<Cut>>"))
self.context_menu.entryconfigure("Copy", command=lambda: self.event_generate("<<Copy>>"))
self.context_menu.entryconfigure("Paste", command=lambda: self.event_generate("<<Paste>>"))
def undo(self, event=None):
if self.steps != 0:
self.steps -= 1
self.delete(0, END)
self.insert(END, self.changes[self.steps])
def redo(self, event=None):
if self.steps < len(self.changes):
self.delete(0, END)
self.insert(END, self.changes[self.steps])
self.steps += 1
def add_changes(self, event=None):
if self.get() != self.changes[-1]:
self.changes.append(self.get())
self.steps += 1
Disclaimer: these are just thoughts that come into my mind on how to implement it.
class History(object):
def __init__(self):
self.l = ['']
self.i = 0
def next(self):
if self.i == len(self.l):
return None
self.i += 1
return self.l[self.i]
def prev(self):
if self.i == 0:
return None
self.i -= 1
return self.l[self.i]
def add(self, s):
del self.l[self.i+1:]
self.l.append(s)
self.i += 1
def current(self):
return self.l[self.i]
Run a thread that every X seconds (0.5?) save the state of the entry:
history = History()
...
history.add(stringval.get())
You can also set up events that save the Entry's status too, such as the pressure of Return.
prev = history.prev()
if prev is not None:
stringvar.set(prev)
or
next = history.next()
if next is not None:
stringvar.set(next)
Beware to set locks as needed.
Update on using this method for Undo/Redo:
I am creating a GUI with lot of frames and each contains at least ten or more 'entry' widgets.
I used the History class and created one history object for each entry field that I had. I was able to store all entry widgets values in a list as done here.
I am using 'trace' method attached to each entry widget which will call 'add' function of History class and store each changes. In this way, I was able to do it without running any thread separately.
But the biggest drawback of doing this is, we cannot do multiple undos/redos with this method.
Issue:
When I trace each and every change of the entry widget and add that to the list, it also 'traces' the change that happens when we 'undo/redo' which means we cannot go more one step back. once u do a undo, it is a change that will be traced and hence the 'undo' value will be added to the list at the end. Hence this is not the right method.
Solution:
Perfect way to do this is by creating two stacks for each entry widget. One for 'Undo' and one for 'redo'. When ever there is a change in entry, push that value into the undo stack. When user presses undo, pop the last stored value from the undo stack and importantly push this one to the 'redo stack'. hence, when the user presses redo, pop the last value from redo stack.
Based on Evgeny's answer with a custom Entry, but added a tkinter StringVar with a trace to the widget to more accurately track when changes are made to its contents (not just when any Key is pressed, which seemed to add empty Undo/Redo items to the stack). Also added a max depth using a Python deque.
If we're changing the contents of the Entry via code rather than keyboard input, we can temporarily disable the trace (e.g. see in the undo method below).
Code:
class CEntry(tk.Entry):
def __init__(self, master, **kw):
super().__init__(master=master, **kw)
self._undo_stack = deque(maxlen=100)
self._redo_stack = deque(maxlen=100)
self.bind("<Control-z>", self.undo)
self.bind("<Control-y>", self.redo)
# traces whenever the Entry's contents are changed
self.tkvar = tk.StringVar()
self.config(textvariable=self.tkvar)
self.trace_id = self.tkvar.trace("w", self.on_changes)
self.reset_undo_stacks()
# USE THESE TO TURN TRACE OFF THEN BACK ON AGAIN
# self.tkvar.trace_vdelete("w", self.trace_id)
# self.trace_id = self.tkvar.trace("w", self.on_changes)
def undo(self, event=None): # noqa
if len(self._undo_stack) <= 1:
return
content = self._undo_stack.pop()
self._redo_stack.append(content)
content = self._undo_stack[-1]
self.tkvar.trace_vdelete("w", self.trace_id)
self.delete(0, tk.END)
self.insert(0, content)
self.trace_id = self.tkvar.trace("w", self.on_changes)
def redo(self, event=None): # noqa
if not self._redo_stack:
return
content = self._redo_stack.pop()
self._undo_stack.append(content)
self.tkvar.trace_vdelete("w", self.trace_id)
self.delete(0, tk.END)
self.insert(0, content)
self.trace_id = self.tkvar.trace("w", self.on_changes)
def on_changes(self, a=None, b=None, c=None): # noqa
self._undo_stack.append(self.tkvar.get())
self._redo_stack.clear()
def reset_undo_stacks(self):
self._undo_stack.clear()
self._redo_stack.clear()
self._undo_stack.append(self.tkvar.get())

how to force matplotlib to update a plot

I am trying to construct a little GUI that has a plot which updates every time a new data sample is read. I would prefer not to run it with a timer, since the data will be arriving at differing intervals. Instead, I'm trying to make an implementation using signals, where the data collection function will emit a signal when data is read, and then the painting function will emit a signal when the painting is completed.
The problem, as it appears right now, is that the canvas is not updating as soon as I call canvas.draw(). When this program runs, data_collect() and paint() alternate sending signals, but the figure is not updated until after I stop the process. How can I force matplotlib to update the figure whenever paint() is called?
What follows is a relatively simple piece of example code which is not optimal, but hopefully will convey the flavor of what I'm trying to do...
N_length = 150;
count = [0];
def sinval(delay):
k = 0;
x = [];
# set up data vector with sinusoidal data in it.
while k < N_length:
x.append(math.sin(2*math.pi*k/N_length));
k += 1;
def next():
time.sleep(delay);
outstring = "%0.3e" % (x[count[0]]);
if (count[0] == (N_length-1)):
count[0] = 0;
else:
count[0] += 1;
return outstring;
return next;
class DesignerMainWindow(QtGui.QMainWindow, Ui_mplMainWindow):
def __init__(self, parent = None):
super(DesignerMainWindow, self).__init__(parent)
self.setupUi(self)
QtCore.QObject.connect(self.mplStartButton, QtCore.SIGNAL("clicked()"), self.start_graph);
QtCore.QObject.connect(self.mplStopButton, QtCore.SIGNAL("clicked()"), self.stop_graph);
QtCore.QObject.connect(self.mplQuitButton, QtCore.SIGNAL("clicked()"), QtGui.qApp, QtCore.SLOT("quit()"));
QtCore.QObject.connect(self, QtCore.SIGNAL("data_collect()"), self.data_collect);
QtCore.QObject.connect(self, QtCore.SIGNAL("paint()"), self.paint);
def start_graph(self):
# generates first "empty" plots
self.user = [];
self.l_user, = self.mpl.canvas.ax.plot([], self.user, label='sine wave');
# set up the axes.
self.mpl.canvas.ax.set_xlim(0, 300);
self.mpl.canvas.ax.set_ylim(-1.1, 1.1);
self.mpl.canvas.draw();
# start the data collection process.
self.delay = 0.05;
self.next = sinval(self.delay);
self.emit(QtCore.SIGNAL('data_collect()'));
def data_collect(self):
outstring = self.next();
self.user.append(float(outstring.split()[0]));
self.l_user.set_data(range(len(self.user)), self.user);
self.emit(QtCore.SIGNAL('paint()'));
def paint(self):
self.mpl.canvas.draw();
self.emit(QtCore.SIGNAL('data_collect()'));
I'd guess that calling QCoreApplication::processEvents after paint() will help. More elegant would be to have a separate QThread for the reading. Take a look at this thread.

Categories