Tkinter Canvas Update Memory Leak - python

I'm seeing a memory leak in the code below. I'm creating and processing my data in separate modules, but this isn't causing the leak from what I can see. I believe it's because I am calling a new instance of drawing class each time I change my scale, although I'm not sure how to correct this issue. I've read this thread, but when I try and implement self.canvas.destroy() method on my code I receive an error. I was wondering what method could be applied to the code below to solve my issue?
Code Snippet:
from Tkinter import *
class Interface_On:
def Interface_Elements(self, master):
self.master=master
self.master.title( "My Canvas")
self.c=Canvas(self.master, width=1000, height=1000, bg='black')
self.c.grid(row=0, column=0)
menubar = Menu(master)
filemenu = Menu(menubar, tearoff=0)
filemenu.add_command(label="New", command=self.Edit_New)
menubar.add_cascade(label="File", menu=filemenu)
master.config(menu=menubar)
drawing_utility_run=Drawing_Utility()
drawing_utility_run.drawer(self.c)
def Edit_New(self):
Export_Poscar = self.Export_Poscar = Toplevel(self.master)
self.Export_Poscar.title('New Ribbon...')
self.Export_Poscar.geometry('300x400')
self.scale_Label= Label(Export_Poscar, width=15, text='scale:')
self.scale_Label.grid(row=2, column=0)
self.scale_Label= Label(Export_Poscar, width=15, text='scale:')
scale_var = StringVar()
self.scale_Spin= Spinbox(Export_Poscar, from_=1, to=1000, increment=1, width=15, command=self.execute_request_immediate, textvariable=scale_var)
self.scale_Spin.grid(row=2, column=2)
def execute_request_immediate(self):
global scale
User_Set_Scale=float(self.scale_Spin.get())
scale=User_Set_Scale
drawing_utility_run=Drawing_Utility()
drawing_utility_run.drawer(self.c)
class Drawing_Utility:
def drawer(self, canvas):
self.canvas=canvas
self.canvas.delete('all')
import Generator #generates my data (imports 'scale' from above where possible)
Generator_run=Generator.Generator()
Generator_run.generator_go()
from Generator import coordinates_x_comp, coordinates_y_comp #imports necessary lists
import Processor #Imports coordinates_x_comp, coordinates_y_comp, cleans and analyses
Process_xy_Data=Processor.Data_Processor()
Process_xy_Data.Process_Data()
from Processor import p_1, p_2
for Line in xrange(len(p_1)):
self.canvas.create_line(p_1[Line],p_2[Line], fill='red', activefill='blue', width=1)
root=Tk()
run_it_canvas=Interface_On()
run_it_canvas.Interface_Elements(root)
root.mainloop()

I'm not sure any of these things will fix the memory leak you say you're observing, but there are a few issues I'd fix that might help:
1. You're using a local variable (drawing_utility_run) to store your Drawing_Utility instances. It's not entirely clear why those instances aren't getting garbage collected once the method they were created in exits, but either way, it seems like you probably want that object to persist, so you should store the reference in the instance namespace, like this:
self.drawing_utility_run=Drawing_Utility()
self.drawing_utility_run.drawer(self.c)
2. When you delete all canvas objects with self.canvas.delete('all') you're relying on the fact that your Tkinter version implements the string 'all' as a recognized constant, which may be the case, but isn't guaranteed. The Canvas.delete function will accept any argument, whether it represents a recognized constant or an tag/ID or not, without throwing an error - for example try self.canvas.delete('blah blah blah'). I.e. you're banking on self.canvas.delete('all') deleting all objects, but it's not obvious to me that it does so. Use the Tkinter constant ALL instead of the string 'all'.
3. Unless you have a very good reason for having your imported modules only exist in the Drawing_Utility instance namespace, you should move all the import statements to the top, in the module-level namespace.
4. Your import statements are redundant:
import Generator #generates my data (imports 'scale' from above where possible)
from Generator import coordinates_x_comp, coordinates_y_comp #imports necessary lists
import Processor #Imports coordinates_x_comp, coordinates_y_comp, cleans and analyses
from Processor import p_1, p_2
You don't need to both import Generator and from Generator import coordinates_x_comp. Just import Generator then refer to Generator.coordinates_x_comp. By using both import statements, you're double-importing Generator.coordinates_x_comp, Processor.p_1 etc.

Related

Tkinter Widget References

I have a question about how to properly address widget attributes from outside the GUI main loop. Here is the gist of my project: I have an HMI, and it runs in its own thread. Outside of the HMI, there will be another thread that is collecting data and writing to hardware. I stripped out a bunch of stuff to create a basic example so you can see what I mean. It consists of a main window, a frame where the operator can select manual or auto, and a notebook that will display temperature probe data. In the real world, there will be varying numbers of probes and additional attributes, which is why I instantiate the probes at the top of the script. In real life, this would come from a SQL database and the GUI would be built dynamically based on database properties.
from tkinter import *
import tkinter as tk
from tkinter import ttk
from threading import Thread
from time import sleep
import random
wProbes= {1: {'Title': 'Indoors','Temperature':0,'MainObject':'','TempLabel':''},2: {'Title': 'Outdoors','Temperature':0,'Object':''}}
controlTemplate={'State':0,'On':0, 'Off':0}
systemControl = {'Auto':controlTemplate,'Manual':controlTemplate}
def gui():
class SystemOperation(tk.LabelFrame):
def __init__(self,parent):
super().__init__(parent)
self.config(text='System Operation',labelanchor='n',bd=2,highlightbackground='black',relief= 'solid',font=("arial", 20))
self.place(width=500, height=900,x=150,y=100)
def autoModeClick():
self.bAutoMode.configure(background='green')
self.bManualMode.configure(background='white')
systemControl['Auto']['State'] = 1
systemControl['Manual']['State'] = 0
def manualModeClick():
self.bManualMode.configure(background='green')
self.bAutoMode.configure(background='white')
systemControl['Auto']['State'] = 0
systemControl['Manual']['State'] = 1
self.bAutoMode = Button(self, text='Auto', background='green', command=autoModeClick)
self.bAutoMode.place(x=100, y=100, width=70)
self.bManualMode = Button(self, text='Manual', command=manualModeClick)
self.bManualMode.place(x=100, y=150, width=70)
class TemperatureProbe(tk.LabelFrame):
def __init__(self,parent,inst):
super().__init__(parent)
self.config(text=wProbes[inst]['Title'],labelanchor='n',bd=2,highlightbackground='black',relief= 'solid',font=("arial", 20))
self.place(width=500, height=300)
self.currentTemp=0
self.actTemp=Label(self,text='Temperature= %d F'%self.currentTemp,width=20)
self.actTemp.place(relx=.5, y=20, anchor='center')
self.pack_propagate(0)
wProbes[inst]['TempLabel']=self.actTemp
class probeNotebook(ttk.Notebook):
def __init__(self,parent):
super().__init__(parent)
self.place(x=900,y=100,width=800,height=700)
for t in sorted(wProbes.keys()):
tabName = wProbes[t]['Title']
self.tabFrame = tk.Frame(self, width=700, height=900, bg='white', borderwidth=2, relief='solid')
self.add(self.tabFrame, text=' %s '%tabName)
self.pFB=TemperatureProbe(self.tabFrame,t)
self.pFB.place(x=100, y=20)
wProbes[t]['MainObject']=self.pFB
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("800x800")
self.title('Test Window')
SystemOperation(self)
probeNotebook(self)
if __name__=="__main__":
app=MainWindow()
app.mainloop()
threadGUI = Thread(target=gui)
threadGUI.start()
while 1:
#A bunch of stuff will be happening here that interfaces with external hardware
if wProbes[1]['MainObject']!='':
randInt=random.randint(10,200)
wProbes[1]['TempLabel'].config(text='Temperature= %d F'%randInt)
wProbes[2]['TempLabel'].config(text='Temperature= %d F' % (randInt+200))
sleep(.5)
So, you can see that a notebook is created, and the tabs are then created based on the number of probes in the probe dictionary. Also in the probe dictionary, I set a reference to the probe container, and then the probe temperature label. If you run the code, you can see the temperatures change correctly based on the probe instances with a test random int.
But what I was really hoping to do is just use the probe MainObject reference and address its child widgets that way, i.e. wProbes[1]['MainObject'].actTemp, because in reality the probe container will have more widgets such as limits and setpoints and warnings. But if creating individual references is the correct way, I'm good with that too. Am I on the right track?
Another thing I wondered about as I go forward is how I can separate the GUI from the process script completely. Imagine the GUI as a standalone app, and a second app that acts like a server and does the heavy lifting and communicates with the GUI. Any basic hints on doing this? I'll do the work in figuring it out, but if you push me in the right direction I'd appreciate it.
Also, if there are criticisms of my project approach in general, I'm all ears. I've done quite a bit of python programming, but this is my first tkinter attempt. Thanks!
(Note: I hope it formatted correctly. I just copied and pasted into my ide, and it was fine. Worst case would be that the indentation is wrong in a couple of places when copied, but give it a try.)

How to change a global variable without global keyword using a button in tkinter?

I am making a rock paper scissors program and I need to change whose turn it is when they click a button, but I do not want to use the global keyword because the program is inside of a function.
Here is an example of what I am trying to do without using the global keyword:
from tkinter import *
root = Tk()
var = 1
def buttonClick():
global var
var += 1
print(var)
button = Button(root, text="button", command=buttonClick).pack()
root.mainloop()
I have tried to write command=(var += 1) but that did not work.
If the whole script is inside a function (including the buttonClick() function) then use the nonlocal keyword:
def buttonClick():
nonlocal var
var += 1
print(var)
If the function is not nested, the only way is to create a global variable and the global keyword in both functions.
No, you indeed can't. It would be possible to change the contents of a global varif it were a list, for example. And then you could write your command as a lambda expression with no full function body.
But this is not the best design at all.
Tkinter event model couples nicely with Python object model - in a way that instead of just dropping your UI components at the toplevel (everything global), coordinated by sparse functions, you can contain everything UI related in a class - even if it will ever have just one instance - that way your program can access the var as "self.var" and the command as "self.button_click" with little danger of things messing up were they should not.
It is just that most documentation and tutorial you find out will have OOP examples of inheriting tkinter objects themselves, and adding your elements on top of the existing classes. I am strongly opposed to that approach: tkinter classes are complex enough, with hundreds of methods and attributes -wereas even a sophisticated program will only need a few dozens of internal states for you to worry about.
Best thing is association: everything you will ever care to access should eb a member of your class. In the start of your program, you instantiate your class, that will create the UI elements and keep references to them:
import tkinter as tk # avoid wildcard imports: it is hard to track what is available on the global namespace
class App:
def __init__(self):
self.root = tk.Tk()
self.var = 1
# keep a refernce to the button (not actually needed, but you might)
self.button = tk.Button(self.root, text="button", command=self.buttonClick)
self.button.pack()
def buttonClick(self):
# the button command is bound to a class instance, so
# we get "self" as the object which has the "var" we want to change
self.var += 1
print(self.var)
def run(self):
self.root.mainloop()
if __name__ == "__main__": # <- guard condition which allows claases and functions defined here to be imported by larger programs
app = App()
app.run()
Yes, you can. Here's a hacky way of doing it that illustrates that it can be done, although it's certainly not a recommended way of doing such things. Disclaimer: I got the idea from an answer to a related question.
from tkinter import *
root = Tk()
var = 1
button = Button(root, text="button",
command=lambda: (globals().update(var=var+1), print(var)))
button.pack()
root.mainloop()

Unable to update Label text in Python Tkinter without calling pack() again

from tkinter import *
from tkinter.ttk import *
root = Tk()
first_run = True
def update(txt):
global first_run
text1 = Label(root, text='')
if first_run:
text1.pack()
text1['text'] = txt
first_run = False
update('1')
update('2')
update('3')
root.mainloop()
When I run this, the text stays at '1', and the following 2 function calls are ignored. I find out that only if I use pack() again then it will be updated, but it creates a duplicate label and I do not want that.
Of course, I know that I am supposed to use a StringVar, but I have been using this method for all other widgets (buttons, label frames etc) and all of them works. I do not know why this particular case does not work.
Running on Python 3.9.9 on Windows 11
You aren't updating the label, you are creating a new label each time the function is called. To update any widget, use the configure method. For that, you need to create the label outside of the function (or, leave it in the function but add logic so that it's only created once). Usually it's best to create it outside the function so that the function is only responsible for the update.
from tkinter import *
from tkinter.ttk import *
root = Tk()
def update(txt):
text1.configure(text=txt)
text1 = Label(root, text='')
text1.pack()
update('1')
update('2')
update('3')
root.mainloop()
Note: since you call your function multiple times before the window is drawn you'll only see the final value. There are plenty of solutions to that on this site. Without knowing more about what your real program looks like it's hard to recommend the best solution to that problem.

Why can't I define (and save) Tkinter fonts inside a function?

Defining a font inside a function and in the main body of the script seems to behave differently, and I can't seem to figure out how it's supposed to work.
For example, the Label in this example ends up being in a larger font, as expected:
from Tkinter import *
from ttk import *
import tkFont
root = Tk()
default = tkFont.Font(root=root, name="TkTextFont", exists=True)
large = default.copy()
large.config(size=36)
style = Style(root)
style.configure("Large.TLabel", font=large)
root.title("Font Test")
main_frame = Frame(root)
Label(main_frame, text="Large Font", style="Large.TLabel").pack()
main_frame.pack()
root.mainloop()
However, if I try to define styles inside a function, it seems like the font gets deleted or garbage collected and is not available by the time the widget needs to use it:
from Tkinter import *
from ttk import *
import tkFont
def define_styles(root):
default = tkFont.Font(root=root, name="TkTextFont", exists=True)
large = default.copy()
large.config(size=36)
style = Style(root)
style.configure("Large.TLabel", font=large)
root = Tk()
root.title("Font Test")
define_styles(root)
main_frame = Frame(root)
Label(main_frame, text="Large Font", style="Large.TLabel").grid(row=0, column=0)
main_frame.pack()
root.mainloop()
Printing out tkFont.names() in the first version just before the main_frame.pack() lists the custom font as font<id>, but printing the same in the second version does not list the custom font outside the define_styles function. Do I have to do something special to save them?
Why can't I put that code in a function? Am I fundamentally misunderstanding something about how Fonts are supposed to be used? tkFont seems to have some kind of font registry, why aren't mine sticking around?
I have no evidence to back this up, but I believe that your large Font object is being garbage collected by Python once define_styles ends. This is because no pure Python objects have any references to it, even though the underlying Tcl implementation is still using it. This is a problem that afflicts Tkinter's PhotoImage class, as well.
The workaround is to keep the object alive by making a long-lived reference to it. Just assign it to any old attribute on the root object, for example.
def define_styles(root):
default = tkFont.Font(root=root, name="TkTextFont", exists=True)
large = default.copy()
large.config(size=36)
style = Style(root)
style.configure("Large.TLabel", font=large)
root.myfont = large
Result:

When importing modules written with tkinter and ttk, stuff doesn't work

I'm new to programming, Python, this website, and actually using these kinds of websites in general, so hear me out.
I've been writing a module for a larger program using the tkinter module and ttk module, and when I import my own module into the main program, for some reason none of the ttk stuff works as it should. I mean, it appears, but the style I've written for it (s=ttk.Style(); s.configure...etc.) doesn't change it in anyway. When I run the module on its own, everything works fine. When it's imported into the main program, it just doesn't.
Not only this, but when using entry boxes, I've only just discovered that the way I'd been told to use them, with, for example, var=StringVar() as the textvariable (which again works fine when the module is run on its own), now just leaves the variable var as empty when var.get() is called. Now I've sorted this by just removing all mention of StringVar() (wish I'd known how redundant these really are), but I'd still like to know why importing them in to the main program causes them to malfunction so badly. I would give you some sample code but there's so much I'd struggle to be selective enough...
I'd appreciate any guidance you can offer.
EDIT: Would giving you something like this have helped?
stackoverflowmodule.py
import sys
from tkinter import *
from tkinter import ttk
import time
from random import randint, choice
class Decimals():
def Question1(self):
DECFrame.destroy()
frame1=ttk.Frame(DECmaster, height=height, width=width, style="NewFrame.TFrame")
frame1.pack()
Q1Label=ttk.Label(frame1, text="Question 1:", style="TitleLabel.TLabel")
Q1Label.grid(column=0, row=0, pady=(50,0))
answer=StringVar()
entry1=ttk.Entry(frame1, textvariable=answer)
entry1.grid(column=0, row=1, pady=(200,0))
# Typing in Hello should give a correct answer.
def Question1Attempt():
attempt=answer.get()
if attempt!="Hello":
print("Incorrect")
else:
print("Correct")
button=ttk.Button(frame1, text="Ok", command=Question1Attempt)
button.grid(column=0, row=2, pady=(30,0))
def Start():
global DECmaster
global s
global DECFrame
global DEC
global width
global height
DECmaster = Tk()
width=str(1000)
height=str(800)
x1=str(0)
y1=str(0)
DECmaster.geometry(width+"x"+height+"+"+x1+"+"+y1)
DECmaster.configure(bg="#8afff0")
s=ttk.Style()
s.configure("NewFrame.TFrame", background="#8afff0")
s.configure("TitleLabel.TLabel", foreground= "blue", background="#8afff0")
DECFrame=ttk.Frame(DECmaster, style="NewFrame.TFrame")
DECFrame.pack()
TitleLabel=ttk.Label(DECFrame, text="Test for Decimals", style="TitleLabel.TLabel")
TitleLabel.grid(column=1, row=0, pady=(50,0), sticky=N)
DEC=Decimals()
button=ttk.Button(DECFrame, text="Start", command=DEC.Question1)
button.grid(column=2, row=2, pady=(200,0), sticky=N)
DECmaster.mainloop()
stackoverflowprogram.py
from tkinter import *
from tkinter import ttk
import time
import stackoverflowmodule
root = Tk()
width=str(1000)
height=str(800)
x1=str(0)
y1=str(0)
##width=str(1228)
##height=str(690)
##x1=str(-1)
##y1=str(-22)
root.geometry(width+"x"+height+"+"+x1+"+"+y1)
root.configure(bg="#8afff0")
s=ttk.Style()
s.configure("NewFrame.TFrame", background="#8afff0")
s.configure("TitleLabel.TLabel", foreground= "blue", background="#8afff0")
Testframe=ttk.Frame(root, height=height, width=width, style="NewFrame.TFrame")
Testframe.pack()
Titlelabel=ttk.Label(Testframe, text="Start Test:", style="TitleLabel.TLabel")
Titlelabel.grid(column=0, row=0, pady=(50,0))
def StartTest():
stackoverflowmodule.Start()
button=ttk.Button(Testframe, text="Start", command=StartTest)
button.grid(column=0, row=1, pady=(100,0))
root.mainloop()
I realise there's an awful lot there, but I couldn't really demonstrate my point without it all. Thanks again.
The root of your problem is that you're creating more than once instance of Tk. A Tkinter app can only have a single instance of of the Tk class, and you must call mainloop exactly once. If you need additional windows you should create instances of Toplevel (http://effbot.org/tkinterbook/toplevel.htm).
If you want to create modules with reusable code, have your modules create subclasses of Frame (or Toplevel if you're creating dialos). Then, your main script will create an instance of Tk, and place these frames in the main window or in subwindows.
If you want to sometimes use your module as a reusable component and sometimes as a runnable program, put the "runnable program" part inside a special if statement:
# module1.py
import Tkinter as tk
class Module1(tk.Frame):
def __init__(self, *args, **kwargs):
label = tk.Label(self, text="I am module 1")
label.pack(side="top", fill="both", expand=True)
# this code will not run if this module is imported
if __name__ == "__main__":
root = tk.Tk()
m1 = Module1(root)
m1.pack(side="top", fill="both", expand=True)
In the above code, if you run it like python module1.py, the code in that final if statement will run. It will create a root window, create an instance of your frame, and make that frame fill the main window.
If, however, you import the above code into another program, the code in the if statement will not run, so you don't get more than one instance of Tk.
Let's assume you have two modules like the above, and want to write a program that uses them, and each should go in a separate window. You can do that by writing a third script that uses them both:
# main.py
import Tkinter as tk
from module1 import Module1
from module2 import Module2
# create the main window; every Tkinter app needs
# exactly one instance of this class
root = tk.Tk()
m1 = Module1(root)
m1.pack(side="top", fill="both", expand=True)
# create a second window
second = tk.Toplevel(root)
m2 = Module2(second)
m2.pack(side="top", fill="both", expand=True)
# run the event loop
root.mainloop()
With the above, you have code in two modules that can be used in three ways: as standalone programs, as separate frames within a single window, or as separate frames within separate windows.
You can't create two instances of tkinter.Tk. If you do, one of two things will happen.
Most of the code in the script may just not run, because it's waiting for the module's mainloop to finish, which doesn't happen until you quit.
If you structure things differently, you'll end up with two Tk instances, only one of which is actually running. Some of the code in your script will happen to find the right Tk instance (or the right actual Tk objects under the covers), because there's a lot of shared global stuff that just assumes there's one Tk "somewhere or other" and manages to find. But other code will find the wrong one, and just have no effect. Or, occasionally, things will have the wrong effect, or cause a crash, or who knows what.
You need to put the top-level application in one place, either the module or the script that uses it, and have the other place access it from there.
One way to do this is to write the module in such a way that its code can be called with a Tk instance. Then, use the __main__ trick so that, if you run the module directly as a script (rather than importing it from another script), it creates a Tk instance and calls that code. Here's a really simple example.
tkmodule.py:
from tkinter import *
def say_hi():
print("Hello, world!")
def create_interface(window):
hi = Button(window, text='Hello', command=say_hi)
hi.pack()
if __name__ == '__main__':
root = Tk()
create_interface(root)
root.mainloop()
tkscript.py:
from tkinter import *
import tkmodule
i = 0
def count():
global i
i += 1
print(i)
def create_interface(window):
countbtn = Button(window, text='Count', command=count)
countbtn.pack()
root = Tk()
create_interface(root)
window = Toplevel(root)
tkmodule.create_interface(window)
root.mainloop()
Now, when you run tkscript.py, it owns one Tk instance, and passes it to its own create_frame and to tkmodule.create_frame. But if you just run tkmodule.py, it owns a Tk instance, which it passes to its own create_frame. Either way, there's exactly one Tk instance, and one main loop, and everyone gets to use it.
Notice that if you want two top-level windows, you have to explicitly create a Toplevel somewhere. (And you don't want to always create one in tkmodule.py, or when you run the module itself, it'll create a new window and leave the default window sitting around empty.)
Of course an even simpler way to do this is to put all of your GUI stuff into modules that never create their own Tk instance, and write scripts that import the appropriate modules and drive them.

Categories