Do I need to use multithreads with Tkinter? [duplicate] - python

My little brother is just getting into programming, and for his Science Fair project, he's doing a simulation of a flock of birds in the sky. He's gotten most of his code written, and it works nicely, but the birds need to move every moment.
Tkinter, however, hogs the time for its own event loop, and so his code won't run. Doing root.mainloop() runs, runs, and keeps running, and the only thing it runs is the event handlers.
Is there a way to have his code run alongside the mainloop (without multithreading, it's confusing and this should be kept simple), and if so, what is it?
Right now, he came up with an ugly hack, tying his move() function to <b1-motion>, so that as long as he holds the button down and wiggles the mouse, it works. But there's got to be a better way.

Use the after method on the Tk object:
from tkinter import *
root = Tk()
def task():
print("hello")
root.after(2000, task) # reschedule event in 2 seconds
root.after(2000, task)
root.mainloop()
Here's the declaration and documentation for the after method:
def after(self, ms, func=None, *args):
"""Call function once after given time.
MS specifies the time in milliseconds. FUNC gives the
function which shall be called. Additional parameters
are given as parameters to the function call. Return
identifier to cancel scheduling with after_cancel."""

The solution posted by Bjorn results in a "RuntimeError: Calling Tcl from different appartment" message on my computer (RedHat Enterprise 5, python 2.6.1). Bjorn might not have gotten this message, since, according to one place I checked, mishandling threading with Tkinter is unpredictable and platform-dependent.
The problem seems to be that app.start() counts as a reference to Tk, since app contains Tk elements. I fixed this by replacing app.start() with a self.start() inside __init__. I also made it so that all Tk references are either inside the function that calls mainloop() or are inside functions that are called by the function that calls mainloop() (this is apparently critical to avoid the "different apartment" error).
Finally, I added a protocol handler with a callback, since without this the program exits with an error when the Tk window is closed by the user.
The revised code is as follows:
# Run tkinter code in another thread
import tkinter as tk
import threading
class App(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.start()
def callback(self):
self.root.quit()
def run(self):
self.root = tk.Tk()
self.root.protocol("WM_DELETE_WINDOW", self.callback)
label = tk.Label(self.root, text="Hello World")
label.pack()
self.root.mainloop()
app = App()
print('Now we can continue running code while mainloop runs!')
for i in range(100000):
print(i)

When writing your own loop, as in the simulation (I assume), you need to call the update function which does what the mainloop does: updates the window with your changes, but you do it in your loop.
def task():
# do something
root.update()
while 1:
task()

Another option is to let tkinter execute on a separate thread. One way of doing it is like this:
import Tkinter
import threading
class MyTkApp(threading.Thread):
def __init__(self):
self.root=Tkinter.Tk()
self.s = Tkinter.StringVar()
self.s.set('Foo')
l = Tkinter.Label(self.root,textvariable=self.s)
l.pack()
threading.Thread.__init__(self)
def run(self):
self.root.mainloop()
app = MyTkApp()
app.start()
# Now the app should be running and the value shown on the label
# can be changed by changing the member variable s.
# Like this:
# app.s.set('Bar')
Be careful though, multithreaded programming is hard and it is really easy to shoot your self in the foot. For example you have to be careful when you change member variables of the sample class above so you don't interrupt with the event loop of Tkinter.

This is the first working version of what will be a GPS reader and data presenter. tkinter is a very fragile thing with way too few error messages. It does not put stuff up and does not tell why much of the time. Very difficult coming from a good WYSIWYG form developer. Anyway, this runs a small routine 10 times a second and presents the information on a form. Took a while to make it happen. When I tried a timer value of 0, the form never came up. My head now hurts! 10 or more times per second is good enough for me. I hope it helps someone else. Mike Morrow
import tkinter as tk
import time
def GetDateTime():
# Get current date and time in ISO8601
# https://en.wikipedia.org/wiki/ISO_8601
# https://xkcd.com/1179/
return (time.strftime("%Y%m%d", time.gmtime()),
time.strftime("%H%M%S", time.gmtime()),
time.strftime("%Y%m%d", time.localtime()),
time.strftime("%H%M%S", time.localtime()))
class Application(tk.Frame):
def __init__(self, master):
fontsize = 12
textwidth = 9
tk.Frame.__init__(self, master)
self.pack()
tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
text='Local Time').grid(row=0, column=0)
self.LocalDate = tk.StringVar()
self.LocalDate.set('waiting...')
tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
textvariable=self.LocalDate).grid(row=0, column=1)
tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
text='Local Date').grid(row=1, column=0)
self.LocalTime = tk.StringVar()
self.LocalTime.set('waiting...')
tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
textvariable=self.LocalTime).grid(row=1, column=1)
tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
text='GMT Time').grid(row=2, column=0)
self.nowGdate = tk.StringVar()
self.nowGdate.set('waiting...')
tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
textvariable=self.nowGdate).grid(row=2, column=1)
tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
text='GMT Date').grid(row=3, column=0)
self.nowGtime = tk.StringVar()
self.nowGtime.set('waiting...')
tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
textvariable=self.nowGtime).grid(row=3, column=1)
tk.Button(self, text='Exit', width = 10, bg = '#FF8080', command=root.destroy).grid(row=4, columnspan=2)
self.gettime()
pass
def gettime(self):
gdt, gtm, ldt, ltm = GetDateTime()
gdt = gdt[0:4] + '/' + gdt[4:6] + '/' + gdt[6:8]
gtm = gtm[0:2] + ':' + gtm[2:4] + ':' + gtm[4:6] + ' Z'
ldt = ldt[0:4] + '/' + ldt[4:6] + '/' + ldt[6:8]
ltm = ltm[0:2] + ':' + ltm[2:4] + ':' + ltm[4:6]
self.nowGtime.set(gdt)
self.nowGdate.set(gtm)
self.LocalTime.set(ldt)
self.LocalDate.set(ltm)
self.after(100, self.gettime)
#print (ltm) # Prove it is running this and the external code, too.
pass
root = tk.Tk()
root.wm_title('Temp Converter')
app = Application(master=root)
w = 200 # width for the Tk root
h = 125 # height for the Tk root
# get display screen width and height
ws = root.winfo_screenwidth() # width of the screen
hs = root.winfo_screenheight() # height of the screen
# calculate x and y coordinates for positioning the Tk root window
#centered
#x = (ws/2) - (w/2)
#y = (hs/2) - (h/2)
#right bottom corner (misfires in Win10 putting it too low. OK in Ubuntu)
x = ws - w
y = hs - h - 35 # -35 fixes it, more or less, for Win10
#set the dimensions of the screen and where it is placed
root.geometry('%dx%d+%d+%d' % (w, h, x, y))
root.mainloop()

Related

Buttons getting misplaced after using background image in Tkinter

Whenever I run this code with a background Image the button grid gets misplaced and pushed towards the bottom. Fortunately, it works as intended when no background is added .I want them to cover the background when executed. Pictures for reference are added below. Your help is highly appreciated.
# importing the module
import tkinter.messagebox
from tkinter import *
import random
# importing the module
# initialising tkinter
class window(Frame):
def __init__(self,master = None):
Frame.__init__(self,master)
self.master = master
# initialising tkinter
# creating the window
root = Tk()
app = window(root)
root.geometry("630x630")
root.title('Odd Even Game')
C = Canvas(root, bg="blue", height=250, width=300)
filename = PhotoImage(file = "BG.png")
background_label = Label(root,image=filename)
background_label.place(x=0, y=0, relwidth=1, relheight=1)
C.pack()
frame = Frame(root)
frame.pack()
# creating the window
# image
level_1e = "p1.png"
level_1o = "pe.png"
level_2e = "r1.png"
level_2o = "re.png"
# image
def create_cards(odd_image,even_image,next_level,fno,order,suc,err,w,h):
rx = random.randint(0,order-1)
ry = random.randint(0,order-1)
for i in range(0,order):
for j in range(0,order):
if i == rx and j == ry:
create_button(i,j,suc,odd_image,next_level,fno,odd_image,w,h)
else:
create_button(i,j,err,even_image,next_level,fno,odd_image,w,h)
def second_level(fno):
fno.pack_forget()
frame2 = Frame(root)
frame2.pack()
suc = "Congratulations! You have cleared level 2..Keep Going Buddy!"
err = "Wrong Answer..Don't give up yet!"
create_cards(level_2o,level_2e,final_level,frame2,4,suc,err,157.5,157.5)
def final_level(fno):
fno.pack_forget()
root.geometry("700x700")
ap = App(root)
# creating a button function
def create_button(x,y,msg,picture,next_level,fno,odd,w,h):
if picture == odd:
image = PhotoImage(file=picture)
click = Button(fno, image=image, width= w, height=h, bd = 0,command = lambda : [score_update(),next_level(fno),tkinter.messagebox.showinfo( "Odd One Out Project",msg)])
click.image = image
click.grid( row = x, column = y)
else:
image = PhotoImage(file=picture)
click = Button(fno, image=image, width= w, height=h, bd = 0,command = lambda : [next_level(fno),tkinter.messagebox.showinfo( "Odd One Out Project",msg)])
click.image = image
click.grid( row = x, column = y)
# creating a button function
def create_frame(fno):
root.geometry("630x630")
fno.pack_forget()
frame = Frame(root)
frame.pack()
suc = "Congratulations! You have cleared level 1..Time to increas[![enter image description here][1]][1]e the difficulty!"
err = "Wrong Answer..Please Try again !!"
create_cards(level_1o,level_1e,second_level,frame,3,suc,err,200,200)
def intro():
root.geometry("630x630")
frame0 = Frame(root)
frame0.pack()
click = Button(frame0,text="Start!" ,command = lambda [create_frame(frame0),tkinter.messagebox.showinfo( "Odd One Out Project","The game has begun!!")])
click.pack()
intro()
# starting the widget
root.mainloop()
# starting the widget
The first image is the error. Second Image is the required output.
Note: I'm still a beginner in Python and Tkinter hence various terms and methods might be something out of my scope. Would be appreciated if taken into consideration.
In case needed, you might know that this is a tkinter project for picking the odd one out image out of A*A grid.
I got the answer myself so will be sharing for future use.
C = Canvas(root, bg="blue", height=250, width=300)
This part draws a canvas of 250*300 dimensions hence does not let the buttons draw over it.
just change it to,
C = Canvas(root, bg="blue", height=0, width=0)
for the desired result

Close Command and Update Command Don't Work With One Button

So for this larger program I'm making I want it so when a user presses a button it closes the dialog windows and updates all the values the user input. Therefore, I have one button do these two things: update the values and close the program. However, trying to combine these two functions doesn't work as when I use both of them only the update() command is called, not the close command. Either works separately btw. Any way to fix this?
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
from tkinter import *
from tkinter.ttk import *
propDiameterInch = 10.5
propDiameterMetric = propDiameterInch*0.0254
class Counter_program():
def __init__(self):
self.window = tk.Tk()
self.window.title("Test")
style = ttk.Style()
style.configure("BW.TLabel", foreground="black", background="white")
#default unit color
unitColor = "slategrey"
boxWidth = 5
# Create some room around all the internal frames
self.window['padx'] = 5
self.window['pady'] = 5
propeller_frame = ttk.LabelFrame(self.window, text="Propeller", relief=tk.RIDGE)
propeller_frame.grid(row=1, column=1, sticky=tk.E + tk.W + tk.N + tk.S)
#propeller diameter
propellerDiameter_label = ttk.Label(propeller_frame, text="Propeller Diameter")
propellerDiameter_label.grid(row=1, column=1, sticky=tk.W + tk.N +tk.S)
propellerDiameter_Units = ttk.Label(propeller_frame, text="inches",foreground=unitColor)
propellerDiameter_Units.grid(row=1, column=3, sticky=tk.W)
propellerDiameter_entry = ttk.Entry(propeller_frame, width=boxWidth)
propellerDiameter_entry.grid(row=1, column=2, sticky=tk.W, pady=3)
propellerDiameter_entry.insert(tk.END, "10")
#now set all global variables from entries - update function
def update():
global propDiameter
propDiameter = propellerDiameter_entry.get()
# Finish button in the lower right corner
#finish_button = ttk.Button(self.window, text = "Submit Input", command = self.window.destroy)
finish_button = ttk.Button(self.window, text = "Submit Input", command=lambda:[update(),self.window.destroy])
finish_button.grid(row=2, column=2)
# Create the entire GUI program
program = Counter_program()
# Start the GUI event loop
program.window.mainloop()
propDiameter
Since your using lambda, its safe to use () with the functions, so just change finish_button to:
finish_button = ttk.Button(self.window, text = "Submit Input", command=lambda:[update(),self.window.destroy()])
Or you could make a new function that does both of this for you, like:
def both():
update()
self.window.destroy()
finish_button = ttk.Button(self.window, text = "Submit Input", command=both)
TIP:
Also its not recommended to use global with OOP, so I recommend you change your code and use proper "methods" and self with OOP for a better experience.
Here is how I think your class should like:
class Counter_program():
def __init__(self):
self.window = tk.Tk()
self.window.title("Test")
style = ttk.Style()
style.configure("BW.TLabel", foreground="black", background="white")
#default unit color
unitColor = "slategrey"
boxWidth = 5
# Create some room around all the internal frames
self.window['padx'] = 5
self.window['pady'] = 5
self.propeller_frame = ttk.LabelFrame(self.window, text="Propeller", relief=tk.RIDGE)
self.propeller_frame.grid(row=1, column=1, sticky=tk.E + tk.W + tk.N + tk.S)
#propeller diameter
self.propellerDiameter_label = ttk.Label(self.propeller_frame, text="Propeller Diameter")
self.propellerDiameter_label.grid(row=1, column=1, sticky=tk.W + tk.N +tk.S)
self.propellerDiameter_Units = ttk.Label(self.propeller_frame, text="inches",foreground=unitColor)
self.propellerDiameter_Units.grid(row=1, column=3, sticky=tk.W)
self.propellerDiameter_entry = ttk.Entry(self.propeller_frame, width=boxWidth)
self.propellerDiameter_entry.grid(row=1, column=2, sticky=tk.W, pady=3)
self.propellerDiameter_entry.insert(tk.END, "10")
# Finish button in the lower right corner
#finish_button = ttk.Button(self.window, text = "Submit Input", command = self.window.destroy)
self.finish_button = ttk.Button(self.window, text = "Submit Input", command=self.both)
self.finish_button.grid(row=2, column=2)
def update(self):
self.propDiameter = self.propellerDiameter_entry.get()
def both(self):
self.update()
self.window.destroy()
Hope this solved the issue, do let me know if any errors or doubts.
Cheers

How to put a widget beneath widgets that are side-by-side using pack?

I try to put the widgets like this:
I don't understand why my code doesn't do that, tried to look for examples online but didn't find a solution and nothing I tried brought me closer to the requested result.
This is my code so far(if you have any comments about anything in the code feel free to tell me because it's my first try with tkinter and GUIs in general):
from Tkinter import *
class box(object):
def __init__ (self, colour,s):
self.root = root
self.listbox = Listbox(self.root, fg = colour, bg = 'black')
self.s = s
self.place_scrollbar()
self.listbox.pack(side = self.s)
def place_scrollbar(self):
scrollbar = Scrollbar(self.root)
scrollbar.pack(side = self.s, fill = Y)
self.listbox.config(yscrollcommand = scrollbar.set)
scrollbar.config(command = self.listbox.yview)
def write(self, contenet):
self.listbox.insert(END, contenet)
root = Tk()
root.resizable(False, False)
boxs = Frame(root)
boxs.pack()
box.root = boxs
server = box("red", LEFT)
client = box("green", RIGHT )
bf = Frame(root)
bf.pack(side = BOTTOM)
entry = Entry(bf,bg ='black', fg = 'white')
entry.pack()
root.mainloop()
You can't do this without using an additional frame to contain the box objects while still using pack, while still maintaining resizability.
But it is more organized in some cases to: use an additional frame to contain your box objects, by initializing it with a parent option.
Right now the widgets inside the box class are children to global root object. Which isn't really a good practice. So let's first pass and use a parent object to be used for widgets inside.
Replace:
def __init__ (self, colour,s):
self.root = root
self.listbox = Listbox(self.root, ...)
...
def place_scrollbar(self):
scrollbar = Scrollbar(self.root)
...
with:
def __init__ (self, parent, colour,s):
self.parent= parent
self.listbox = Listbox(self.parent, ...)
...
def place_scrollbar(self):
scrollbar = Scrollbar(self.parent)
...
This makes it so that you now need to initialize the object like the following:
server = box(root, "red", LEFT)
client = box(root, "green", RIGHT )
Now that we can pass a parent widget, let's create a parent frame to contain them. Actually, there's an un-used frame already, boxs let's use that by passing it as the parent as opposed to root:
server = box(boxs, "red", LEFT)
client = box(boxs, "green", RIGHT )
Now everything looks fine, optionally if you want to make it so that entry occupies as much left space as possible currently add fill='x' as an option to the pack of both the entry and the frame that contains it:
bf.pack(side = BOTTOM, fill='x')
...
entry.pack(fill='x')
Your whole code should look like:
from Tkinter import *
class box(object):
def __init__ (self, parent, colour,s):
self.parent = parent
self.listbox = Listbox(self.parent, fg = colour, bg = 'black')
self.s = s
self.place_scrollbar()
self.listbox.pack(side = self.s)
def place_scrollbar(self):
scrollbar = Scrollbar(self.parent)
scrollbar.pack(side = self.s, fill = Y)
self.listbox.config(yscrollcommand = scrollbar.set)
scrollbar.config(command = self.listbox.yview)
def write(self, contenet):
self.listbox.insert(END, contenet)
root = Tk()
root.resizable(False, False)
boxs = Frame(root)
boxs.pack()
box.root = boxs
server = box(boxs, "red", LEFT)
client = box(boxs, "green", RIGHT )
bf = Frame(root)
bf.pack(side = BOTTOM, fill='x')
entry = Entry(bf,bg ='black', fg = 'white')
entry.pack(fill='x')
root.mainloop()
Or: use grid instead of pack (with columnspan=2 option for entry).
General Answer
More generally putting a widget beneath two widgets that are side-by-side can be done by:
Encapsulating the side-by-side widgets with a frame, and then simply putting the frame above the other widget:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
def main():
root = tk.Tk()
side_by_side_widgets = dict()
the_widget_beneath = tk.Entry(root)
frame = tk.Frame(root)
for name in {"side b", "y side"}:
side_by_side_widgets[name] = tk.Label(frame, text=name)
side_by_side_widgets[name].pack(side='left', expand=True)
frame.pack(fill='x')
the_widget_beneath.pack()
root.mainloop()
if __name__ == '__main__':
main()
Using grid:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
def main():
root = tk.Tk()
side_by_side_widgets = dict()
the_widget_beneath = tk.Entry(root)
for index, value in enumerate({"side b", "y side"}):
side_by_side_widgets[value] = tk.Label(root, text=value)
side_by_side_widgets[value].grid(row=0, column=index)
the_widget_beneath.grid(row=1, column=0, columnspan=2)
root.mainloop()
if __name__ == '__main__':
main()
Without using additional frames, by calling pack for the_widget_beneath with side='bottom' as the first pack call, as in Bryan's comment:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
def main():
root = tk.Tk()
side_by_side_widgets = dict()
the_widget_beneath = tk.Entry(root)
the_widget_beneath.pack(side='bottom')
for name in {"side b", "y side"}:
side_by_side_widgets[name] = tk.Label(root, text=name)
side_by_side_widgets[name].pack(side='left', expand=True)
root.mainloop()
if __name__ == '__main__':
main()
You can more easily notice reliability to global objects by creating a global main method, and add main-body of your script there and call:
...
def main():
root = Tk()
root.resizable(False, False)
boxs = Frame(root)
boxs.pack()
box.root = boxs
server = box(boxs, "red", LEFT)
client = box(boxs, "green", RIGHT )
bf = Frame(root)
bf.pack(side = BOTTOM, fill='x')
entry = Entry(bf,bg ='black', fg = 'white')
entry.pack(fill='x')
root.mainloop()
if __name__ == '__main__':
main()
How to put a widget beneath two widgets that are side-by-side using pack?
For a very simple layout like in your diagram, you simply need to pack the thing on the bottom first. That is because pack uses a "cavity" model. Each widget is organized in an unfilled cavity. Once that widget has been placed, that portion of the cavity is filled, and is unavailable for any other widgets.
In your case, you want the bottom cavity to be filled with the entry widget, so you should pack it first. Then, in the remaining upper cavity you can place your two frames side-by side, one on the left and one on the right.
For example:
import Tkinter as tk
root = tk.Tk()
entry = tk.Entry(root)
frame1 = tk.Frame(root, width=100, height=100, background="red")
frame2 = tk.Frame(root, width=100, height=100, background="green")
entry.pack(side="bottom", fill="x")
frame1.pack(side="left", fill="both", expand=True)
frame2.pack(side="right", fill="both", expand=True)
root.mainloop()
In the body of your question things get a bit more complicated, as you don't just have three widgets like your title suggests, you have several, with some being packed in the root and some being packed elsewhere, with pack statements scattered everywhere.
When using pack, it's best to group widgets into vertical or horizontal slices, and not mix top/bottom with left/right within the same group. It almost seems like you're trying to do that with your boxes, but then you don't -- your "box" is actually two widgets.
Bottom line: be organized. It also really, really helps if all of your pack (or place or grid) statements for a given parent are all in the same block of code. When you scatter them around it makes it impossible to visualize, and impossible to fix. Also, make sure that widgets have the appropriate parents.

Annoying grey flicker during updates in ttk GUI

I am trying to populate my screen with some data that refreshes every given time interval. I am using Python3, themed tkinter. Every time my screen updates I see grey flickerings on screen for every label. Is there a way to avoid this?
P.S : I am calling the 'after' method to refresh data.
UPDATE: Here is some example code:
def button1Click(self):
self.top = Toplevel(width=600,height=400)
self.top.title("XYZ ORGANIZATION")
self.frame1 = Frame(self.top,bg='#009999')
self.frame1.pack()
self.noOfEmp = Label(self.frame1,text = "Number_Of_Employees : ", font =('Verdana',9, 'bold'),bg='#009999',fg = '#000000')
self.noOfEmp.grid(row=1,column=0,sticky=W,padx=0,pady=5)
self.TeamLabel = Label(self.frame1,text = "Team Name : ", font =('Verdana',9, 'bold'),bg='#009999',fg = '#000000')
self.TeamLabel.grid(row=2,column=0,sticky=W,padx=0,pady=5)
self.text = Text(self.frame1, bg='#009999')
self.text.grid(row=8,columnspan=17)
self.old_emp = 0
self.EFile = open('/abc','r').readlines()
for line in self.EFile:
if line.startswith('EmpTotal:'):
self.Tot_Emp = int(line.split()[1])
break
t1 = threading.Thread(target=self.__make_layout, args = ())
t1.daemon = True
t1.start()
t2 = threading.Thread(target=self.ProcEmp,args = ())
t2.daemon = True
t2.start()
def self.__make_layout:
self.CLabelVal = Label(self.frame1,text = CSpace, font=('Verdana',9),bg='#009999',fg = '#000000')
self.MLabelVal = Label(self.frame1,text = MSpace , font =('Verdana',9),bg='#009999',fg = '#000000')
self.Label1Val.grid(row=4,column=1,sticky=W+E+N+S,padx=5,pady=5)
self.Label2Val.grid(row=5,column=1,sticky=W+E+N+S,padx=5,pady=5)
self.frame1.after(5000,self.__make_layout)
Part of the problem is that you keep stacking more and more widgets on top of each other. You should create the labels exactly once, and then change what they display every five seconds, instead of creating new widgets every five seconds.
There's also the problem that you're creating the labels in a thread. Tkinter isn't thread safe. Any code that creates or modifies a widget needs to be in the main thread. To update the labels you don't need threads, though you can use a thread to change what actually gets displayed.
def __make_layout(self):
self.CLabelVal = Label(...,text = CSpace, ...)
self.MLabelVal = Label(...,text = MSpace, ...)
self.Label1Val.grid(...)
self.Label2Val.grid(...)
def __update_layout(self):
self.CLabelVal.configure(text=CSpace)
self.MLabelVal.configure(text=MSpace)
self.after(5000, self.__update_layout)
I made a small program based on what you provided.
This is what i got. I chose 500ms instead because i did not want to wait that long. I ran two internet videos at the same time and had no issues.
So my best guess you have a slow video card or over loaded computer.
from tkinter import *
class MyClass:
frame1 = Tk()
poll = 0
def __init__(self):
self.frame1.after(500, self.__make_layout)
def __make_layout(self):
self.poll += 1
CSpace = "Poll count = "*20
MSpace = str(self.poll)
self.CLabelVal = Label(self.frame1, text=CSpace, font=('Verdana', 9), bg='#009999', fg='#000000')
self.MLabelVal = Label(self.frame1, text=MSpace, font=('Verdana', 9), bg='#009999', fg='#000000')
self.CLabelVal.grid(row=4, column=1, sticky=W+E+N+S, padx=5, pady=5)
self.MLabelVal.grid(row=5, column=1, sticky=W+E+N+S, padx=5, pady=5)
print(CSpace, MSpace)
return self.frame1.after(500, self.__make_layout)
MyClass()
mainloop()
This doesn't create more labels and uses the "textvariable" update feature.
from tkinter import *
class MyClass:
frame1 = Tk()
poll = 0
textstg = StringVar()
CSpace = "Poll count"
def __init__(self):
self.frame1.after(500, self.__make_layout)
self.CLabelVal = Label(self.frame1, text=self.CSpace, font=('Verdana', 9), bg='#009999', fg='#000000')
self.MLabelVal = Label(self.frame1, textvariable=self.textstg, font=('Verdana', 9), bg='#009999', fg='#000000')
self.CLabelVal.grid(row=4, column=1, sticky=W+E+N+S, padx=5, pady=5)
self.MLabelVal.grid(row=5, column=1, sticky=W+E+N+S, padx=5, pady=5)
def __make_layout(self):
self.poll += 1
self.textstg.set(str(self.poll))
return self.frame1.after(50, self.__make_layout)
MyClass()
mainloop()

How do you run your own code alongside Tkinter's event loop?

My little brother is just getting into programming, and for his Science Fair project, he's doing a simulation of a flock of birds in the sky. He's gotten most of his code written, and it works nicely, but the birds need to move every moment.
Tkinter, however, hogs the time for its own event loop, and so his code won't run. Doing root.mainloop() runs, runs, and keeps running, and the only thing it runs is the event handlers.
Is there a way to have his code run alongside the mainloop (without multithreading, it's confusing and this should be kept simple), and if so, what is it?
Right now, he came up with an ugly hack, tying his move() function to <b1-motion>, so that as long as he holds the button down and wiggles the mouse, it works. But there's got to be a better way.
Use the after method on the Tk object:
from tkinter import *
root = Tk()
def task():
print("hello")
root.after(2000, task) # reschedule event in 2 seconds
root.after(2000, task)
root.mainloop()
Here's the declaration and documentation for the after method:
def after(self, ms, func=None, *args):
"""Call function once after given time.
MS specifies the time in milliseconds. FUNC gives the
function which shall be called. Additional parameters
are given as parameters to the function call. Return
identifier to cancel scheduling with after_cancel."""
The solution posted by Bjorn results in a "RuntimeError: Calling Tcl from different appartment" message on my computer (RedHat Enterprise 5, python 2.6.1). Bjorn might not have gotten this message, since, according to one place I checked, mishandling threading with Tkinter is unpredictable and platform-dependent.
The problem seems to be that app.start() counts as a reference to Tk, since app contains Tk elements. I fixed this by replacing app.start() with a self.start() inside __init__. I also made it so that all Tk references are either inside the function that calls mainloop() or are inside functions that are called by the function that calls mainloop() (this is apparently critical to avoid the "different apartment" error).
Finally, I added a protocol handler with a callback, since without this the program exits with an error when the Tk window is closed by the user.
The revised code is as follows:
# Run tkinter code in another thread
import tkinter as tk
import threading
class App(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.start()
def callback(self):
self.root.quit()
def run(self):
self.root = tk.Tk()
self.root.protocol("WM_DELETE_WINDOW", self.callback)
label = tk.Label(self.root, text="Hello World")
label.pack()
self.root.mainloop()
app = App()
print('Now we can continue running code while mainloop runs!')
for i in range(100000):
print(i)
When writing your own loop, as in the simulation (I assume), you need to call the update function which does what the mainloop does: updates the window with your changes, but you do it in your loop.
def task():
# do something
root.update()
while 1:
task()
Another option is to let tkinter execute on a separate thread. One way of doing it is like this:
import Tkinter
import threading
class MyTkApp(threading.Thread):
def __init__(self):
self.root=Tkinter.Tk()
self.s = Tkinter.StringVar()
self.s.set('Foo')
l = Tkinter.Label(self.root,textvariable=self.s)
l.pack()
threading.Thread.__init__(self)
def run(self):
self.root.mainloop()
app = MyTkApp()
app.start()
# Now the app should be running and the value shown on the label
# can be changed by changing the member variable s.
# Like this:
# app.s.set('Bar')
Be careful though, multithreaded programming is hard and it is really easy to shoot your self in the foot. For example you have to be careful when you change member variables of the sample class above so you don't interrupt with the event loop of Tkinter.
This is the first working version of what will be a GPS reader and data presenter. tkinter is a very fragile thing with way too few error messages. It does not put stuff up and does not tell why much of the time. Very difficult coming from a good WYSIWYG form developer. Anyway, this runs a small routine 10 times a second and presents the information on a form. Took a while to make it happen. When I tried a timer value of 0, the form never came up. My head now hurts! 10 or more times per second is good enough for me. I hope it helps someone else. Mike Morrow
import tkinter as tk
import time
def GetDateTime():
# Get current date and time in ISO8601
# https://en.wikipedia.org/wiki/ISO_8601
# https://xkcd.com/1179/
return (time.strftime("%Y%m%d", time.gmtime()),
time.strftime("%H%M%S", time.gmtime()),
time.strftime("%Y%m%d", time.localtime()),
time.strftime("%H%M%S", time.localtime()))
class Application(tk.Frame):
def __init__(self, master):
fontsize = 12
textwidth = 9
tk.Frame.__init__(self, master)
self.pack()
tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
text='Local Time').grid(row=0, column=0)
self.LocalDate = tk.StringVar()
self.LocalDate.set('waiting...')
tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
textvariable=self.LocalDate).grid(row=0, column=1)
tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
text='Local Date').grid(row=1, column=0)
self.LocalTime = tk.StringVar()
self.LocalTime.set('waiting...')
tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
textvariable=self.LocalTime).grid(row=1, column=1)
tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
text='GMT Time').grid(row=2, column=0)
self.nowGdate = tk.StringVar()
self.nowGdate.set('waiting...')
tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
textvariable=self.nowGdate).grid(row=2, column=1)
tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
text='GMT Date').grid(row=3, column=0)
self.nowGtime = tk.StringVar()
self.nowGtime.set('waiting...')
tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
textvariable=self.nowGtime).grid(row=3, column=1)
tk.Button(self, text='Exit', width = 10, bg = '#FF8080', command=root.destroy).grid(row=4, columnspan=2)
self.gettime()
pass
def gettime(self):
gdt, gtm, ldt, ltm = GetDateTime()
gdt = gdt[0:4] + '/' + gdt[4:6] + '/' + gdt[6:8]
gtm = gtm[0:2] + ':' + gtm[2:4] + ':' + gtm[4:6] + ' Z'
ldt = ldt[0:4] + '/' + ldt[4:6] + '/' + ldt[6:8]
ltm = ltm[0:2] + ':' + ltm[2:4] + ':' + ltm[4:6]
self.nowGtime.set(gdt)
self.nowGdate.set(gtm)
self.LocalTime.set(ldt)
self.LocalDate.set(ltm)
self.after(100, self.gettime)
#print (ltm) # Prove it is running this and the external code, too.
pass
root = tk.Tk()
root.wm_title('Temp Converter')
app = Application(master=root)
w = 200 # width for the Tk root
h = 125 # height for the Tk root
# get display screen width and height
ws = root.winfo_screenwidth() # width of the screen
hs = root.winfo_screenheight() # height of the screen
# calculate x and y coordinates for positioning the Tk root window
#centered
#x = (ws/2) - (w/2)
#y = (hs/2) - (h/2)
#right bottom corner (misfires in Win10 putting it too low. OK in Ubuntu)
x = ws - w
y = hs - h - 35 # -35 fixes it, more or less, for Win10
#set the dimensions of the screen and where it is placed
root.geometry('%dx%d+%d+%d' % (w, h, x, y))
root.mainloop()

Categories

Resources