I have a Tkinter program which I want to pause for 3 seconds.
time.sleep doesn't work and the after method doesn't do exactly what I want to.
here is an example code:
from Tkinter import *
def waithere():
print "waiting..."
root = Tk()
print "1"
root.after(3000,waithere)
print "2"
root.mainloop()
output:
1
2
*3 seconds*
waiting...
the output i want to have:
1
waiting...
*3 seconds*
2
thanks.
Normally it's a very bad idea to have a GUI wait for something. That's imply not how event-based programs work. Or more accurately, GUIs are already in a perpetual wait state, and you don't want to block that with your own waiting.
That being said, tkinter has a way to wait until certain things happen. For example, you can use one of the "wait" functions, such as wait_variable, wait_window, or wait_visibility.
Assuming that you wanted waithere to do the waiting, you could use wait_variable to do the waiting, and after to set the variable after a given amount of time.
Here's the solution based on your original code:
from Tkinter import *
def waithere():
var = IntVar()
root.after(3000, var.set, 1)
print("waiting...")
root.wait_variable(var)
root = Tk()
print "1"
waithere()
print "2"
root.mainloop()
The advantage to using these methods is that your code is still able to respond to events while it is waiting.
I found a way like that, i hope it helps you:
from tkinter import *
def waitToShow():
index = 1
while index < 11:
l1.config(text=index)
l1.after(1000)
l1.update()
index += 1
win = Tk()
l1 = Label(win)
l1.pack()
waitToShow()
win.mainloop()
Just for future reference, refrain from using long or infinite loops in Tkinter; they will prevent the UI from responding to user events (AKA freezing). The method I was taught was to periodically update the field using the after() function.
The after() function creates an alarm-callback meaning when called (with the right parameters) it will queue a call to the target method (in the example below def update(self) with our entered delay. You can use a boolean in the class to exit the loop. Create on on __init__ and then when set to False don't call after() anymore.
Here is an example creating a class inheriting Tkinter.Frame to inherit the functionality.
try:
import tkinter as tk
except:
import Tkinter as tk
import datetime
class DelayedUpdateUI(tk.Frame):
def __init__(self, master=None, **kw):
# Create widgets, if any.
tk.Frame.__init__(self, master=master, **kw)
self.timeStr = tk.StringVar()
self.lblTime = tk.Label(self, textvariable=self.timeStr)
self.lblTime.grid()
# Call update to begin our recursive loop.
self.update()
def update(self):
self.timeStr.set(datetime.datetime.now())
# We use after( milliseconds, method_target ) to call our update
# method again after our entered delay. :)
self.after(1000, self.update)
if __name__ == '__main__':
root = tk.Tk()
DelayedUpdateUI(root).grid()
root.mainloop()
A suggestion based on Bryan's answer:
I understand the recommended way from an event-based perspective, but it does not feel very intuitive to me. I have to look up the trick every time I need it. Therefore, I have created a small mixin class that makes usage a bit more intuitive:
import tkinter as tk
class TkWaitMixin:
"""Simple wait timer that makes Tk waiting functionality
more intiutive. Applies the recommended way according to
https://stackoverflow.com/a/51770561/12646289.
"""
def start_wait_timer(self, milliseconds):
self.resume = tk.BooleanVar(value=False)
self.master.after(milliseconds, self.resume.set, True)
# Assume master attribute is available:
# https://stackoverflow.com/a/53595036/12646289
def wait_on_timer(self):
self.master.wait_variable(self.resume)
Example usage:
import tkinter as tk
class MyWindow(tk.Tk, TkWaitMixin):
def __init__(self, master):
self.master = master
self.message_label = tk.Label('')
self.message_label.pack(padx=50, pady=50)
def show_message(self, message, milliseconds):
self.start_wait_timer(milliseconds)
self.message_label['text'] = message
self.wait_on_timer()
self.message_label['text'] = ''
root = tk.Tk()
mywin = MyWindow(master=root)
mywin.show_message('Hello world', 2000)
root.mainloop()
Obviously, this will only be of use if you use classes in your tkinter code. Also note that the master attribute should be available in the main class to which the mixin is added.
Edit
Alternatively, usage can be made even easier with a context manager:
import tkinter as tk
class TkWait:
def __init__(self, master, milliseconds):
self.duration = milliseconds
self.master = master
def __enter__(self):
self.resume = tk.BooleanVar(value=False)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.master.after(self.duration, self.resume.set, True)
self.master.wait_variable(self.resume)
Note that the waiting starts when the context manager is exited.
Example usage:
import tkinter as tk
class MyWindow(tk.Tk):
def __init__(self, master):
self.master = master
self.message_label = tk.Label('')
self.message_label.pack(padx=50, pady=50)
def show_message(self, message, milliseconds):
with TkWait(self.master, milliseconds):
self.message_label['text'] = message
self.message_label['text'] = ''
root = tk.Tk()
mywin = MyWindow(master=root)
mywin.show_message('Hello world', 2000)
root.mainloop()
You forgot to do the () at root.after(3000,waithere <<<<-)
from tkinter import *
def waithere():
print("waiting...")
root = Tk()
print("1")
root.after(3000,waithere())
print("2")
root.mainloop()
Related
I have a Python program using Tkinter to show a value (var peso inside capturarpeso() function) in realtime.
But the while loop in capturarPeso() doesn't work, the loop only works the first time then the script is "waiting".
If I remove the TK component, it works perfectly. I simplified the script:
import tkinter as tk
from tkinter import *
import threading
import random
def capturarPeso():
global peso
while True:
peso = random.randrange(0, 101, 2)
print (peso)
return(peso)
def capturarPesoHilo():
hilo = threading.Thread(target=capturarPeso, name=None, group=None, args=(), kwargs=None, daemon=True)
hilo.start()
hilo.join()
class ActualizarPeso(Label):
def __init__(self, parent, *args, **kwargs):
Label.__init__(self, parent, *args, **kwargs)
self.tick()
def tick(self):
self.config(text= peso)
self.after(500, self.tick)
capturarPesoHilo()
window = tk.Tk()
window.title('Capturador pesos')
window.resizable(width=False, height=False)
pesoLabel = ActualizarPeso(window, font="Arial 60", fg="red", bg="black", width=8, height= 1)
pesoLabel.grid(row=15, column=0)
window.mainloop()
Any ideas on how to continue? Thank you
The function captuarPeso() has a return statement which will exit the while loop, this is why you only get 1 number printed to the screen.
Removing the return makes it so your program is stuck in that while loop which only prints peso because when you do hilo.join() to a thread it's actually waiting for the thread to exit before continuing, and since we got rid of the return in the first step, the thread never exits and so it's again stuck in a loop. To fix this I changed your while loop to while self.peso != -999: and after calling .mainloop() you set self.peso = -999 which will tell the program: the user has exited the Tkinter interface, exit my loop.
Since you used a class to put some of your tkinter gui in, why not put it all in? Generaly most people would put the entire tkinter interface in a class, I've gone ahead and restructured the program for you but tried to leave as much as the original by itself so you can analyze it and see how it works.
import tkinter as tk
import threading
import random
import time
class ActualizarPeso:
def __init__(self):
self.window = tk.Tk()
self.window.title('Capturador pesos')
self.window.resizable(width=False, height=False)
self.pesoLabel = self.crearLabel()
self.peso = 0
self.tick()
hilo1 = self.capturarPesoHilo()
self.window.mainloop()
self.peso = -999
hilo1.join()
def crearLabel(self):
pesoLabel = tk.Label(self.window, font="Arial 60", fg="red", bg="black", width=8, height=1)
pesoLabel.grid(row=15, column=0)
return pesoLabel
def tick(self):
self.pesoLabel.config(text=self.peso)
self.pesoLabel.after(500, self.tick)
def capturarPeso(self):
while self.peso != -999:
self.peso = random.randrange(0, 101, 2)
print(self.peso)
time.sleep(1)
def capturarPesoHilo(self):
hilo = threading.Thread(target=self.capturarPeso)
hilo.start()
return hilo
ActualizarPeso()
Let me know if you need something explained, and Happy Holidays!
I am stumped as to why the Display class isn't running in the following code. I call it using Display(root) in main(). The main class runs just fine. I don't get any errors in terminal.
#!/usr/bin/python
import alsaaudio as aa
import audioop
import Tkinter as tk
import tkFont
class Display():
def __init__(self, parent):
self.parent = parent
self._geom = '200x200+0+0'
parent.geometry("{0}x{1}+0+0".format(parent.winfo_screenwidth(), parent.winfo_screenheight()))
parent.overrideredirect(1)
parent.title('Listen')
parent.configure(background='#000000')
parent.displayFont = tkFont.Font(family="Unit-Bold", size=150)
def printMessage(self, parent, messageString):
self.message = tk.Message(self.parent, text=messageString, bg="#000000", font=parent.displayFont, fg="#777777", justify="c")
self.message.place(relx=.5, rely=.5, anchor="c")
def main():
root = tk.Tk()
window = Display(root)
# Set up audio
data_in = aa.PCM(aa.PCM_CAPTURE, aa.PCM_NONBLOCK, 'hw:1')
data_in.setchannels(2)
data_in.setrate(44100)
data_in.setformat(aa.PCM_FORMAT_S16_LE)
data_in.setperiodsize(256)
while True:
# Read data from device
l,data = data_in.read()
if l:
# catch frame error
try:
max_vol=audioop.rms(data,2)
scaled_vol = max_vol//4680
print scaled_vol
window.printMessage(root, scaled_vol)
except audioop.error, e:
if e.message !="not a whole number of frames":
raise e
root.mainloop()
if __name__ == '__main__':
main()
Also, both classes run perfectly separately. I'm just having problems using them together.
I think it's because there is an infinite loop (while True) that does not allow the execution to reach to Tk event loop root.mainloop().
Using threads might help to solve this issue. You can run the data input loop in a separate thread, and run Tk event loop in the main thread as it is.
Update: I ran the code (Python 2.7.6 on Linux 3.13.0-24-generic #47-Ubuntu SMP, pyalsaaudio==0.8.2) and it was showing the data. However it could be possible that using threads on different platforms behave differently. I'm having doubts about threads in this case now, because in the comments you mentioned that even the print statement is not being called. Another approach is not to use threads and the while True loop, but to use the mainloop from Tkinter itself.
Also please not that Display.printMessage is creating a new message widget on each call. This will end up with many message widgets showing (and occupying memory). To avoid this, I'd suggest using Tkinter variables, as used on the second code example using Tkinter mainloop.
Solution 1 Using threads (minimum change to code in the question):
#!/usr/bin/python
import alsaaudio as aa
import audioop
import Tkinter as tk
import tkFont
from threading import Thread, Event
class Display():
def __init__(self, parent):
self.parent = parent
self._geom = '200x200+0+0'
parent.geometry("{0}x{1}+0+0".format(parent.winfo_screenwidth(), parent.winfo_screenheight()))
parent.overrideredirect(1)
parent.title('Listen')
parent.configure(background='#000000')
parent.displayFont = tkFont.Font(family="Unit-Bold", size=150)
def printMessage(self, messageString):
self.message = tk.Message(self.parent, text=messageString, bg="#000000", font=self.parent.displayFont, fg="#777777", justify="c")
self.message.place(relx=.5, rely=.5, anchor="c")
def setup_audio(window, stop_event):
data_in = aa.PCM(aa.PCM_CAPTURE, aa.PCM_NONBLOCK, 'hw:1')
data_in.setchannels(2)
data_in.setrate(44100)
data_in.setformat(aa.PCM_FORMAT_S16_LE)
data_in.setperiodsize(256)
while not stop_event.is_set():
# Read data from device
l,data = data_in.read()
if l:
# catch frame error
try:
max_vol=audioop.rms(data,2)
scaled_vol = max_vol//4680
print scaled_vol
window.printMessage(scaled_vol)
except audioop.error, e:
if e.message !="not a whole number of frames":
raise e
def main():
root = tk.Tk()
window = Display(root)
stop_event = Event()
audio_thread = Thread(target=setup_audio, args=[window, stop_event])
audio_thread.start()
try:
root.mainloop()
finally:
stop_event.set()
audio_thread.join()
if __name__ == '__main__':
main()
Changes:
Moved reading sound loop into a separate function that accepts a threading Event to signal stop the loop
Removed the parent arg from Display.printMesssage() definition and use self.parent instead. So in the setup_autdio function when calling the printMessage(), there is no need to pass in root as an argument anymore.
Solution 2 Using Tkinter mainloop, and run all in the main thread (note that I also changed the Display.printMessage() class, mainly for performance improvements):
#!/usr/bin/python
import alsaaudio as aa
import audioop
import Tkinter as tk
import tkFont
class Display():
def __init__(self, parent):
self.parent = parent
self._geom = '200x200+0+0'
parent.geometry("{0}x{1}+0+0".format(parent.winfo_screenwidth(), parent.winfo_screenheight()))
parent.overrideredirect(1)
parent.title('Listen')
parent.configure(background='#000000')
parent.displayFont = tkFont.Font(family="Unit-Bold", size=150)
# using a Tkinter variable to display different messages, on the same widget
self.messageVar = tk.StringVar()
self.message = tk.Message(self.parent, textvar=self.messageVar, bg="#000000", font=self.parent.displayFont, fg="#777777", justify="c")
self.message.place(relx=.5, rely=.5, anchor="c")
def printMessage(self, messageString):
self.messageVar.set(messageString)
def update_ui_from_audio(data_in, window):
l, data = data_in.read()
if l:
# catch frame error
try:
max_vol=audioop.rms(data,2)
scaled_vol = max_vol//4680
print scaled_vol
window.printMessage(scaled_vol)
except audioop.error, e:
if e.message !="not a whole number of frames":
raise e
def main():
root = tk.Tk()
window = Display(root)
# Set up audio
data_in = aa.PCM(aa.PCM_CAPTURE, aa.PCM_NONBLOCK, 'hw:1')
data_in.setchannels(2)
data_in.setrate(44100)
data_in.setformat(aa.PCM_FORMAT_S16_LE)
data_in.setperiodsize(256)
def audio_read_loop():
update_ui_from_audio(data_in, window)
root.after(1, audio_read_loop)
root.after(1, audio_read_loop)
root.mainloop()
if __name__ == '__main__':
main()
Changes:
Move the code that reads data from PCM device, and updates the UI into a separate function named update_ui_from_audio
Display.printMessage uses a StringVar to update the message, so only 1 message widget is used.
Replace the while True loop, with another function that calls the update_ui_from_audio function, and asks Tkinter to call it again in a milisecond (hence, acting like a loop).
I am having a problem with a fairly simple app.
It performs properly, but I would like it to perform a little slower.
The idea is to randomly generate a name from a list, display it, then remove it fromthe list every time a button is clicked.
To make it a little more interesting, I want the program to display several names before
picking the last one. I use a simple for loop for this. However, the code executes so quickly, the only name that winds up displaying is the last one.
using time.sleep() merely delays the display of the last name. no other names are shown.
here is my code:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from tkinter import *
import random
import time
class Application(Frame):
def __init__(self, master):
""" Initialize the frame. """
super(Application, self).__init__(master)
self.grid()
self.name_list = ["Thorin","Tyler","Jose","Bryson","Joe"]
self.create_widget()
def create_widget(self):
self.lbl = Label(self)
self.lbl["text"] = "Click to spin"
self.lbl["font"] = ("Arial", 24)
self.lbl.grid()
self.bttn = Button(self)
self.bttn["text"]= "Spin"
self.bttn["command"] = self.spin
self.bttn.grid()
def spin(self):
if self.name_list:
for i in range(5):
index = random.randrange(len(self.name_list))
self.lbl["text"] = self.name_list[index]
self.lbl.grid()
self.name_list.pop(index)
else:
self.lbl["text"] = "No more names"
self.lbl.grid()
def main():
root = Tk()
root.title("Click Counter")
root.geometry("600x600")
app = Application(root)
root.mainloop()
if __name__ == '__main__':
main()
This is a pretty common class of problems related to GUI programming. The heart of the issue is the window drawing manager. As long as your function is executing, the drawing manager is frozen; updating the label's text will have no apparent effect until your function ends. So if you have a for loop with a sleep(1) command inside, all it will do is freeze everything for five seconds before updating with your final value when the function finally ends.
The solution is to use the after method, which tells Tkinter to call the specified function at some point in the future. Unlike sleep, this gives the drawing manager the breathing room it requires to update your window.
One possible way to do this is to register six events with after: five for the intermediate name label updates, and one for the final name change and pop.
def spin(self):
def change_name():
index = random.randrange(len(self.name_list))
self.lbl["text"] = self.name_list[index]
self.lbl.grid()
def finish_spinning():
index = random.randrange(len(self.name_list))
self.lbl["text"] = self.name_list[index]
self.lbl.grid()
self.name_list.pop(index)
if self.name_list:
name_changes = 5
for i in range(name_changes):
self.after(100*i, change_name)
self.after(100*name_changes, finish_spinning)
else:
self.lbl["text"] = "No more names"
self.lbl.grid()
(disclaimer: this is only a simple example of how you might use after, and may not be suitable for actual use. In particular, it may behave badly if you press the "spin" button repeatedly while the names are already spinning. Also, the code duplication between change_name and finish_spinning is rather ugly)
The code as it is can show the same item twice since it chooses a new random number each time and so will choose the same number part of the time. Note that you do not pop until after the loop which means that each time you run the program you will have one less name which may or may not be what you want. You can use a copy of the list if you want to keep it the same size, and/or random.shuffle on the list and display the shuffled list in order. Also you only have to grid() the label once,
class Application():
def __init__(self, master):
""" Initialize the frame. """
self.master=master
self.fr=Frame(master)
self.fr.grid()
self.name_list = ["Thorin","Tyler","Jose","Bryson","Joe"]
self.ctr=0
self.create_widget()
def create_widget(self):
self.lbl = Label(self.master width=30)
self.lbl["text"] = "Click to spin"
self.lbl["font"] = ("Arial", 24)
self.lbl.grid()
self.bttn = Button(self.master)
self.bttn["text"]= "Spin"
self.bttn["command"] = self.spin
self.bttn.grid()
def change_label(self):
self.lbl["text"] = self.name_list[self.ctr]
self.ctr += 1
if self.ctr < 5:
self.master.after(1000, self.change_label)
else:
self.ctr=0
def spin(self):
if self.name_list and 0==self.ctr: # not already running
random.shuffle(self.name_list)
self.change_label()
else:
self.lbl["text"] = "No more names"
if __name__ == '__main__':
root = Tk()
root.title("Click Counter")
root.geometry("600x600")
app = Application(root)
root.mainloop()
I have a program written in IDLE3.3 and tkinter where I don't know where to place the mainloop(). The program creates a systray icon that creates a little note if you click on "new note" in the context menu. If there is the line "self.root.mainloop()" at the end of Note.init(), the note is shown, but only one note. if I create a second one, the first note is dead and nothing further happens.
But if I don't call the mainloop() in the init-method, i see that there a several notes created because it is printed in the shell.
So the question is, where should I place the mainloop so that every newly created notw is shown and works? Sorry for that possibly stupid question but I can't figure it out.
from tkinter import *
import sys
from PyQt4.QtGui import *
import threading
class Note():
yellow=["#e7e37c","#d9d574"]
def __init__(self,noteset=None, properties=None):
self.root=Tk()
self.noteset=noteset
self.properties=properties
self.screen_width = self.root.winfo_screenwidth()
self.screen_height = self.root.winfo_screenheight()
print("No initial properties to load => creating new note")
self.notecolor=self.yellow[0]
self.gripcolor=self.yellow[1]
self.root.overrideredirect(1)
self.text=""
self.font="arial"
self.fontsize=10
self.sizeX=250
self.sizeY=200
self.posX=int(self.screen_width/2 - self.sizeX/2)
self.posY=int(self.screen_height/2 - self.sizeY/2)
self.root.wm_geometry("%sx%s+%s+%s" %(self.sizeX, self.sizeY, self.posX, self.posY) )
self.root.wm_attributes("-topmost",1)
self.GUI()
self.bindings()
self.root.mainloop()
def bindings(self):
self.frmGRIP.bind("<ButtonPress-1>", self.StartMove)
self.frmGRIP.bind("<ButtonRelease-1>", self.StopMove)
self.frmGRIP.bind("<B1-Motion>", self.OnMotion)
def StartMove(self, event):
self.startx = event.x
self.starty = event.y
def OnMotion(self, event):
mousex,mousey=self.root.winfo_pointerxy()
self.root.geometry("+%s+%s" % (mousex-self.startx, mousey-self.starty))
def StopMove(self, event):
self.posX = self.root.winfo_x()
self.posY = self.root.winfo_y()
def GUI(self):
self.frmTOP=Frame(master=self.root,height=15)
self.frmBOTTOM=Frame(master=self.root,width=300,height=300)
self.frmGRIP=Frame(self.frmTOP,bg=self.gripcolor,height=15)
self.frmRESIZE=Frame(self.frmBOTTOM,width=300,height=10)
self.frmTEXT=Frame(self.frmBOTTOM,bg=self.notecolor,width=300,height=300)
self.frmRESIZE_empty=Frame(self.frmRESIZE,bg=self.notecolor,height=10)
self.frmRESIZE_grip=Frame(self.frmRESIZE,bg=self.gripcolor,width=10,height=10)
self.frmTOP.pack(fill=X,expand=NO)
self.frmBOTTOM.pack(side=BOTTOM,fill=BOTH,expand=YES)
self.frmGRIP.pack(side=LEFT,fill=X,expand=YES)
self.frmRESIZE.pack(side=BOTTOM,fill=X)
self.frmTEXT.pack(side=BOTTOM,fill=BOTH,expand=YES)
self.frmRESIZE_empty.pack(side=LEFT,fill=X,expand=YES)
self.frmRESIZE_grip.pack(side=LEFT,expand=NO)
self.T=Text(self.frmTEXT,
height=6,width=30,
bd=0,wrap=WORD,pady=3,padx=5,
bg=self.notecolor,undo=1,
font=(self.font,self.fontsize)
)
self.T.insert(END,self.text)
self.T.pack(fill=BOTH,expand=YES)
class Noteset():
def __init__(self):
self.notes = []
def newNote(self):
note=Note(noteset=self)
self.notes.append(note)
print(self.notes)
return note
class Main():
def __init__(self):
self.N=Noteset()
app = QApplication(sys.argv)
trayIcon = QSystemTrayIcon(QIcon("J:\\python\\SimpleNotes.ico"), app)
menu = QMenu()
ActionNewNote = menu.addAction("new Note")
ActionNewNote.triggered.connect(self.newNote)
trayIcon.setContextMenu(menu)
trayIcon.show()
app.exec()
def newNote(self):
self.N.newNote()
Main()
You cannot successfully use Qt and Tkinter together. Also, if you remove Qt from the above you have the additional problem that you should not create more than once instance of Tk.
To answer your specific question, mainloop is normally the very last line of code that you execute. Since it is an infinite loop, any code after you call mainloop won't execute until the main window is destroyed.
The normal structure for a Tkinter application goes something like this:
import Tkinter as tk
class MyApp(...):
def __init__(self, root, ...):
...
...
root = tk.Tk()
myApp(root)
root.mainloop()
Its not a good idea to mix gui frameworks as each of the mainloops block each other, your better off to code the whole thing in one or the other.
I'm trying to connect a progress bar to a function for my project.
This is what I have so far but im pretty sure it does nothing:
def main():
pgBar.start()
function1()
function2()
function3()
function4()
pgBar.stop()
Here is the code where I make my progress bar if that helps at all:
pgBar = ttk.Progressbar(window, orient = HORIZONTAL, length=300, mode = "determinate")
pgBar.place(x=45, y=130)
I have been doing some research and understand that the tkinter window freezes when running a function or something like that. Is there a way I could "unfreeze" the window at the end of each function that is called inside the main one?
Since tkinter is single threaded, you need another thread to execute your main function without freezing the GUI. One common approach is that the working thread puts the messages into a synchronized object (like a Queue), and the GUI part consumes this messages, updating the progress bar.
The following code is based on a full detailed example on ActiveState:
import tkinter as tk
from tkinter import ttk
import threading
import queue
import time
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.queue = queue.Queue()
self.listbox = tk.Listbox(self, width=20, height=5)
self.progressbar = ttk.Progressbar(self, orient='horizontal',
length=300, mode='determinate')
self.button = tk.Button(self, text="Start", command=self.spawnthread)
self.listbox.pack(padx=10, pady=10)
self.progressbar.pack(padx=10, pady=10)
self.button.pack(padx=10, pady=10)
def spawnthread(self):
self.button.config(state="disabled")
self.thread = ThreadedClient(self.queue)
self.thread.start()
self.periodiccall()
def periodiccall(self):
self.checkqueue()
if self.thread.is_alive():
self.after(100, self.periodiccall)
else:
self.button.config(state="active")
def checkqueue(self):
while self.queue.qsize():
try:
msg = self.queue.get(0)
self.listbox.insert('end', msg)
self.progressbar.step(25)
except Queue.Empty:
pass
class ThreadedClient(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
for x in range(1, 5):
time.sleep(2)
msg = "Function %s finished..." % x
self.queue.put(msg)
if __name__ == "__main__":
app = App()
app.mainloop()
Since the original example on ActiveState is a bit messy IMO (the ThreadedClient is quite coupled with the GuiPart, and things like controlling the moment to spawn the thread from the GUI are not as straightforward as they could be), I have refactored it and also added a Button to start the new thread.
To understand the 'freezing' you need to understand mainloop(). Calling this method starts the tkinter event loop. The main thread is responsible for this loop. Therefore, when your work intensive function runs in the main thread, it is also interfering with the mainloop. To prevent this you can use a secondary Thread to run your function. It's recommended that secondary threads are not given access to tkinter objects. Allen B.Taylor, author of mtTkinter, states:
The problems stem from the fact that the _tkinter module attempts to
gain control of the main thread via a polling technique when
processing calls from other threads.
If it succeeds, all is well. If it fails (i.e., after a timeout), the
application receives an exception with the message: "RuntimeError:
main thread is not in main loop".
You can have the secondary thread put information into a Queue. Then have a function that checks the Queue every x milliseconds, within the mainloop, via the after() method.
First, decide what you want the value of the Progressbar's maximum option to be.
This is the Progressbar's maximum indicator value (how many units are required to fill the Progressbar).
For example, you could set maximum=4 and then put the appropriate indicator value into the Queue after each of your four functions. The main thread can then retrieve these values (from the Queue) to set the progress via a tkinter.IntVar().
(Note that if you use progbar.step(), the Progressbar resets to 0 (empty) at the end, instead of reaching 4 (completely filled).)
Here's a quick look at how you can use a tkinter.IntVar() with a Progressbar:
int_var = tkinter.IntVar()
pb_instance = ttk.Progressbar(root, maximum=4)
pb_instance['variable'] = int_var
pb_instance.pack()
# completely fill the Progressbar
int_var.set(4)
# get the progress value
x = int_var.get()
Here's an example based on your own (renamed the "main" function "arbitrary"):
import time
import threading
try: import tkinter
except ImportError:
import Tkinter as tkinter
import ttk
import Queue as queue
else:
from tkinter import ttk
import queue
class GUI_Core(object):
def __init__(self):
self.root = tkinter.Tk()
self.int_var = tkinter.IntVar()
progbar = ttk.Progressbar(self.root, maximum=4)
# associate self.int_var with the progress value
progbar['variable'] = self.int_var
progbar.pack()
self.label = ttk.Label(self.root, text='0/4')
self.label.pack()
self.b_start = ttk.Button(self.root, text='Start')
self.b_start['command'] = self.start_thread
self.b_start.pack()
def start_thread(self):
self.b_start['state'] = 'disable'
self.int_var.set(0) # empty the Progressbar
self.label['text'] = '0/4'
# create then start a secondary thread to run arbitrary()
self.secondary_thread = threading.Thread(target=arbitrary)
self.secondary_thread.start()
# check the Queue in 50ms
self.root.after(50, self.check_que)
def check_que(self):
while True:
try: x = que.get_nowait()
except queue.Empty:
self.root.after(25, self.check_que)
break
else: # continue from the try suite
self.label['text'] = '{}/4'.format(x)
self.int_var.set(x)
if x == 4:
self.b_start['state'] = 'normal'
break
def func_a():
time.sleep(1) # simulate some work
def func_b():
time.sleep(0.3)
def func_c():
time.sleep(0.9)
def func_d():
time.sleep(0.6)
def arbitrary():
func_a()
que.put(1)
func_b()
que.put(2)
func_c()
que.put(3)
func_d()
que.put(4)
que = queue.Queue()
gui = GUI_Core() # see GUI_Core's __init__ method
gui.root.mainloop()
If all you want is something that indicates to the user that there is activity
you can set the Progressbar's mode option to 'indeterminate'.
The indicator bounces back and forth in this mode (the speed relates to the maximum option).
Then you can call the Progressbar's start() method directly before starting the secondary thread;
and then call stop() after secondary_thread.is_alive() returns False.
Here's an example:
import time
import threading
try: import tkinter
except ImportError:
import Tkinter as tkinter
import ttk
else: from tkinter import ttk
class GUI_Core(object):
def __init__(self):
self.root = tkinter.Tk()
self.progbar = ttk.Progressbar(self.root)
self.progbar.config(maximum=4, mode='indeterminate')
self.progbar.pack()
self.b_start = ttk.Button(self.root, text='Start')
self.b_start['command'] = self.start_thread
self.b_start.pack()
def start_thread(self):
self.b_start['state'] = 'disable'
self.progbar.start()
self.secondary_thread = threading.Thread(target=arbitrary)
self.secondary_thread.start()
self.root.after(50, self.check_thread)
def check_thread(self):
if self.secondary_thread.is_alive():
self.root.after(50, self.check_thread)
else:
self.progbar.stop()
self.b_start['state'] = 'normal'
def func_a():
time.sleep(1) # simulate some work
def func_b():
time.sleep(0.3)
def func_c():
time.sleep(0.9)
def func_d():
time.sleep(0.6)
def arbitrary():
func_a()
func_b()
func_c()
func_d()
gui = GUI_Core()
gui.root.mainloop()
→ Progressbar reference
You must be using:
self.pgBar.step(x)
where 'x' is the amount to be increased in progressbar.
for this to get updated in your UI you have to put
self.window.update_idletasks() after every self.pgBar.step(x) statement