Tkinter issue with multiple callback functions and use of after() - python

I am working on a very long piece of code and one which I would like to take GUI input more than once in multiple windows.
The code is too long and wouldn't make much sense if I put it in here, but here's the layout of my code:
callback1():
do something
tkinter stuff, using callback1()
...
lots of other code (none to do with tkinter)
...
callback2():
do something else
tkinter stuff using callback2()
some more code (none to do with tkinter)
mainloop()
Sorry for the vague structure, but this is more or less the layout of my code. The problem is though, that mainloop() never seems to be reached, and my other code in between the callbacks relies on the input from the GUI and so it crashes.
I've tried:
callback1():
do something
after(500, callback1)
tkinter stuff, using callback1()
...
lots of other code (none to do with tkinter)
...
callback2():
do something else
after(500, callback1)
tkinter stuff using callback2()
some more code (none to do with tkinter)
after(0, callback1)
after(0, callback2)
mainloop()
But had no luck. Can anyone help me with the correct usage of after() (or any other function) so that I can just get the GUI to display?

GUI frameworks like tkinter are event-driven. They start running when they start their mainloop(). Everything before that is basically set-up and housekeeping.
After the mainloop starts, the only pieces of your code that they run are callbacks that you've attached to interface elements like buttons or to timers with after.
These callbacks must be relatively short and should not take a long time. The GUI toolkit calls the function you provided to handle an event in the mainloop. So while your callback is running, no further events are being processed and the GUI is effectively frozen. If that only takes a little while (say 50 milliseconds) nobody will notice.
But if you want to run a computation that takes several seconds to complete, you cannot do that in a callback without freezing the GUI.
You could consider doing the long-running job in a second thread. But in CPython only one thread at a time can be executing Python bytecode for technical reasons. However (especially in Python 3) threads are forced to relinquish the CPU regularly, so this might be a workable solution.
Another solution is to farm long-running jobs out to a separate process using multiprocessing. In this case you do have to set-up communication between the processes via e.g. a Queue.

You can't organize your code like that. Presumably, you have something similar to this:
import tkinter as tk
mainwindow = tk.Tk()
mainwindow.geometry("300x200")
mainwindow.title("MyWindow")
label = tk.Label(mainwindow, text="Enter stuff, then click the button.")
label.pack()
var = tk.StringVar()
entry = tk.Entry(mainwindow, textvariable=var)
entry.pack()
#callback1():
def onclick():
#do something with var.get() ?
pass
button = tk.Button(mainwindow, text="click me", command=onclick)
button.pack()
#lots of other code (none to do with tkinter):
print("The value I need is: {}".format(var.get()))
mainwindow.mainloop()
The output of that program is:
The value I need is:
You need to put your code in either the callback function, or another function that is called by the callback function:
def my_func():
print("The value I need: {}".format(var.get()))
def onclick():
#do something with var.get()
my_func()
button = tk.Button(mainwindow, text="click me", command=onclick)
Now, the output is something like:
The value I need is: 10
tkinter is a GUI framework, and it responds to events that happen in your window. The way you respond to an event is to wrap your code in a callback function and pass the function to tkinter, which will then call the function at the proper time. You do not write arbitrary code in the middle of a tkinter program.
The after() method does not seem appropriate, because it essentially says:
After the mainloop starts, execute this function.
You can delay the execution for however many seconds you want--but that doesn't mean that the user will have entered the data you need by then. What you want to do is execute your code after the user enters their data, and usually you do that by responding to a button click, where the button says, Submit, or some other event.

Related

How do I Update Tkinter constantly without freezing everything else?

I need to send updates to tkinter from another thread, which should be handled immediately. Sadly Tkinter freezes whenever I try multithreading.
My Attempt:
I've read through several Tkinter-threaded pages but didn't find anything that works well, since most of them try creating a new thread on button.click(), which doesn't help here.
I tried not calling .mainloop() and instead calling the update functions myself, whenever an update comes in:
#GUI
def update(self, string):
self._interactor.config(text=string)
#interactor is a button
self._tk.update_idletasks()
self._tk.update()
This works fine until I use a loop with sleep() in the master to constantly update the text. GUI and master are frozen during sleep().
So I tried to use a threaded timer as discussed here.
#MASTER
gui=gui_remote(self)
def changetext():
text=self.gettextsomewhere()
self._gui.update(text)
loop=RepeatedTimer(5, changetext)
But this just leads to the following error, thrown by Tkinter:
RuntimeError: main thread is not in main loop
Having a hard time on how to solve this. Is it possible to call a GUI class on main thread and still have proper access to its functions?
How I got to this point:
For my project, I need a button, which represents several Buttons.
Every y (eg. 1.5) seconds, the displayed text should be updated to a new one from the outside.
Also, I want to keep GUI, Controller and Data separated, (using blueprint methods) so that later adjustments on each of them will be easier.
I already got it to work, using TK's .after() function, but I had to use GUI and controlling functions closely together.
My Plan
Have a GUI class, which is updateable from another object via simple public functions. The other object (the master) should be able to create a GUI object and call the GUI's update functions with new data every y seconds.
When the GUI button is clicked, simply call a certain method at master every time:
#GUI example
from tkinter import Tk, Button, Frame, Label
class gui_sample:
def __init__(self, master):
"""This is the very simple GUI"""
self._master=master
self._tk=Tk()
self._interactor= Button(self._tk, text="Apfelsaft", command=self._click)
self._interactor.pack()
self._tk.mainloop()
def update(self, string):
"""Handle interactor update"""
self._interactor.config(text=string)
def _click(self):
self._master.click()
#MASTER
from gui_module import *
class Controller:
def __init__(self):
self._gui=gui_sample(self)
self._run()
def _run(self):
#call this every 5 seconds
new_text=self.gettextfromsomewhere()
self._gui.update(new_text)
def click():
#do something
pass
#this code is just a blueprint it probably does nothing
My Problem:
I don't want the master to use TK functions since I might switch to another UI module later and keep the master's functionality. The master will constantly loop through what's being displayed next and needs to be accessible at the same time. Using loops with sleep() isn't a good idea since they will block both, the master and the GUI. Calling .mainloop() is also problematic since it will block all other programs. The gui should always respond to updates and not ask for them.

How to easily avoid Tkinter freezing?

I developed a simple Python application doing some stuff, then I decided to add a simple GUI using Tkinter.
The problem is that, while the main function is doing its stuff, the window freezes.
I know it's a common problem and I've already read that I should use multithreads (very complicated, because the function updates the GUI too) or divide my code in different function, each one working for a little time.
Anyway I don't want to change my code for such a stupid application.
My question is: is it possible there's no easy way to update my Tkinter window every second? I just want to apply the KISS rule!
I'll give you a pseudo code example below that I tried but didn't work:
class Gui:
[...]#costructor and other stuff
def refresh(self):
self.root.update()
self.root.after(1000,self.refresh)
def start(self):
self.refresh()
doingALotOfStuff()
#outside
GUI = Gui(Tk())
GUI.mainloop()
It simply will execute refresh only once, and I cannot understand why.
Thanks a lot for your help.
Tkinter is in a mainloop. Which basically means it's constantly refreshing the window, waiting for buttons to be clicked, words to be typed, running callbacks, etc. When you run some code on the same thread that mainloop is on, then nothing else is going to perform on the mainloop until that section of code is done. A very simple workaround is spawning a long running process onto a separate thread. This will still be able to communicate with Tkinter and update it's GUI (for the most part).
Here's a simple example that doesn't drastically modify your psuedo code:
import threading
class Gui:
[...]#costructor and other stuff
def refresh(self):
self.root.update()
self.root.after(1000,self.refresh)
def start(self):
self.refresh()
threading.Thread(target=doingALotOfStuff).start()
#outside
GUI = Gui(Tk())
GUI.mainloop()
This answer goes into some detail on mainloop and how it blocks your code.
Here's another approach that goes over starting the GUI on it's own thread and then running different code after.
'Not responding' problem can be avoided using Multithreading in python using the thread module.
If you've defined any function, say combine() due to which the Tkinter window is freezing, then make another function to start combine() in background as shown below:
import threading
def combine():
...
def start_combine_in_bg():
threading.Thread(target=combine).start()
Now instead of calling combine(), you have to call start_combine_in_bg()
I did this and now my window is not freezing so I shared it here. Remember you can run only one thread at a time.
Have a look for reference: stackoverflow - avoid freezing of tkinter window using one line multithreading code
Here's how I managed to work around this issue (maybe it's quite dirty I don't know):
Whenever I update my tkinter window through the doingALotOfStuff() function, I call myTk.update().
It avoids freezing for me.

Tkinter window says (not responding) but code is running

I have a program that runs a long process after you click an action button. As the process is running the root window will say that it is not responding even though I know the program is running in the background. This program is going to be released to a few people that I work with and I want to make sure they don't freak out and close the window when they see this. The solution I have is sitting a root.update in the loop of the process that is running but I am not sure this was the best fix.
Using the python 3.3
Here is a sample of the code so you get an idea of what I am doing, this is called from the main loop:
def combine(boxes_to, boxes_from, frame):
to_value,to_pos = gui.checkBoxes(boxes_to)
from_value,from_pos = gui.checkBoxes(boxes_from)
frame.destroy()
running = Label(root,text="Running please do not close..",font = (16))
running.pack()
root.update()
map_to = open("map_to",'r')
for line in map_to:
root.update()
process(line)
running.destroy()
map_to.close()
finish = Button(root, text="Done",command=gui.stop)
finish.pack()
While you can call root.update() in your loop, this will still produce some (potentially) undesirable side-effects.
The program may act laggy, meaning it takes a long time to respond to user input.
You will only be able to run this one action. Any other action has to wait for this to finish.
As an alternative I would suggest that you implement simple multi-threading. Python multithreading is pretty simple, and will prevent both of these drawbacks. You will be able to execute your long running code, while still providing a clean and responsive UI.
If your application is trivially parallelizable, you could use multiple threads to decrease running time. Ex. Thread 1 handles entries 1-100, while thread 2 handles entries 101-200.
The best you can do here is to use multithreading in Python. Here's how to do this:
Let's say you have a function named combine() due to which the window is freezing, which is being used as a command for a button named 'btn' as shown here:
btn = Button(root, text="Click Me", command=combine)
Now, when btn is pressed you might be getting the 'not responding' problem. To fix this, edit the code as shown below:
import threading
btn = Button(root, text="Click Me", command=threading.Thread(target=combine).start)
Here threading.Thread creates a separate thread in which the combine() method is executed, so the GUI can continue to keep responding while the command is being executed.

Delay in method executing commands

I am having an issue with the portion of code below:
my_var = tk.StringVar(value="start")
my_label = tk.Label(self.root, textvariable=my_var)
def my_method(input):
my_var.set("working")
#make a method call to an external Python API; can take a few mins to finish
my_var.set("complete")
tk.Button(self.root, text="GO!", command=lambda: my_method(input))
I have a button that, when clicked, should execute my_method which should initially change the text in my_label to working, process some actions in an external API, then update the label to complete. What is actually happening is my_method is called, the label's text DOES NOT change, the API call is made which takes a few mins, then my label is updated to complete. I am assuming that it processes both the set commands quickly, but I only see the final set value.
Is there a reason why this is happening (have I done something wrong) and is there a solution to it? If this is expected behaviour, could someone explain why it is the case?
Whilst writing this, I thought about maybe launching a thread to execute the initial set call to see if that will work. I'd prefer a simpler solution though.
Tkinter needs you to return promptly from the callback, so that it can get back to the processing it needs to be doing in Tk.mainloop. Without being able to do this processing, it can't update your label's text. (Nor can it handle normal processing events like dragging the window, etc.)
The callbacks should never hang onto the main thread for long periods of time, as a result. If there's some time-consuming processing to do for them, it should be done in a separate thread.

What is the difference between root.destroy() and root.quit()?

In Python using tkinter, what is the difference between root.destroy() and root.quit() when closing the root window?
Is one prefered over the other? Does one release resources that the other doesn't?
root.quit() causes mainloop to exit. The interpreter is still intact, as are all the widgets. If you call this function, you can have code that executes after the call to root.mainloop(), and that code can interact with the widgets (for example, get a value from an entry widget).
Calling root.destroy() will destroy all the widgets and exit mainloop. Any code after the call to root.mainloop() will run, but any attempt to access any widgets (for example, get a value from an entry widget) will fail because the widget no longer exists.
quit() stops the TCL interpreter. This is in most cases what you want, because your Tkinter-app will also stop. It can be a problem, if you e.g. call your app from idle. idle is itself a Tkinker-app, so if you call quit() in your app and the TCL interpreter gets terminated, idle will also terminate (or get confused ).
destroy() just terminates the mainloop and deletes all widgets. So it seems to be safer if you call your app from another Tkinter app, or if you have multiple mainloops."
taken from http://www.daniweb.com/forums/thread66698.html
The tkinter.Tk "quit" method exits the "mainloop" event-handler, and "destroy" destroys all the embedded widgets and only then exits the "mainloop". So is "destroy" the better of the two? Well, sometimes not. If "destroy" fails to destroy all the widgets for some reason, then "mainloop" is never exited and Python locks up. It can be better to just let Python shut things down in an orderly manner at the end of the script.
For example, if you embed a Matplotlib plot in a Tkinter window, that is useful because Matplotlib's own widgets are somewhat clunky to use. Unfortunately, if you then try to close the window by clicking the usual "X" in the title-bar, the window closes alright but leaves Python running. If the script had been started from a terminal, you'd have to mash Ctrl-C for a couple of minutes to get the prompt back. The reason being that the window-close event is bound to "destroy" which does not destroy the Matplotlib objects but leaves them orphaned.
The fix is to bind the window-close event to "quit" instead. But... if the script is started in a Tkinter-based IDE like IDLE, then that creates a new problem in that the window does not close because IDLE holds Tkinter running. So now "destroy" must be added after the mainloop. Finally, all is well.
Below is a minimal example of a Matplotlib plot that can be inverted with a press of a Tkinter button. Its window can be closed without problems. But if the windows-close event had been bound to "destroy" instead of "quit" then a locked-up Python process would remain.
#!/usr/bin/env python3
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
root = tk.Tk()
data, = plt.plot([0,5,3,4,-5,3])
canvas = FigureCanvasTkAgg(plt.gcf(), master=root)
invert = lambda: (data.set_ydata(-data.get_ydata()), canvas.draw())
tk.Button(master=root, text="Invert", command=invert).pack()
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=1)
root.protocol("WM_DELETE_WINDOW", root.quit)
root.mainloop()
root.destroy()
Edit: I'll add that by binding the window-close event to both methods, you can avoid adding a line after the "mainloop", should that be desirable for some reason:
root.protocol("WM_DELETE_WINDOW", lambda: (root.quit(), root.destroy()))
My experience with root.quit() and root.destroy() ...
I have a dos python script, which calls a tkinter script (to choose from set of known values from combobox), then returns to the dos script to complete other things.
The TKinter script I've added onto the parent script. I may convert all to tkinter, but a combo works for the time being. It works in the following way:
To get rid of the windows box after selection was implemented, I needed to
1) root.quit() inside the callback function where all my keypresses were being processed.
2) root.destroy() after mainloop to destroy the windows box.
If I used root.destroy() inside the callback, I got an error message saying tkinter objects were no longer accessable.
Without the root.destroy() after mainloop, the windows box STAYED ONSCREEN until the whole parent script had completed.

Categories