Is tkwait wait_variable/wait_window/wait_visibility broken? - python

I recently started to use tkwait casually and noticed that some functionality only works under special conditions. For example:
import tkinter as tk
def w(seconds):
dummy = tk.Toplevel(root)
dummy.title(seconds)
dummy.after(seconds*1000, lambda x=dummy: x.destroy())
dummy.wait_window(dummy)
print(seconds)
root = tk.Tk()
for i in [5,2,10]:
w(i)
root.mainloop()
The code above works just fine and as expected:
The for loop calls the function
The function runs and blocks the code for x seconds
The window gets destroyed and the for loop continues
But in a more event driven environment these tkwait calls gets tricky. The documentation states quote:
If an event handler invokes tkwait again, the nested call to tkwait
must complete before the outer call can complete.
Instead of an output of >>5 >>2 >>10 you will get >>10 >>2 >>5 because the nested call blocks the inner and the outer will block the inner. I suspect a nested event loop or an equivalent of the mainloop processes events in the normal fashion while waiting.
Am I doing something wrong by using this feature? Because if you think about it, nearly all tkinter dialog windows are using this feature and I've never read about this behavior before.
An event driven example might be:
import tkinter as tk
def w(seconds):
dummy = tk.Toplevel(root)
dummy.title(seconds)
dummy.after(seconds*1000, lambda x=dummy: x.destroy())
dummy.wait_window(dummy)
print(seconds)
root = tk.Tk()
btn1 = tk.Button(
root, command=lambda : w(5), text = '5 seconds')
btn2 = tk.Button(
root, command=lambda : w(2), text = '2 seconds')
btn3 = tk.Button(
root, command=lambda : w(10), text = '10 seconds')
btn1.pack()
btn2.pack()
btn3.pack()
root.mainloop()
As an additional problem that raises with wait_something is that it will prevent your process to finish if the wait_something never was released.

Basically, you need great care if you're using an inner event loop because:
Conditions that would terminate the outer event loop aren't checked for until the inner event loop(s) are finished.
It's really quite easy to end up recursively entering an inner event loop by accident.
The recursive entry problem is usually most easily handled by disabling the path that enters the event loop while the inner event loop runs. There's often an obvious way to do this, such as disabling the button that you'd click.
The condition handling is rather more difficult. In Tcl, you'd handle it by restructuring things slightly using a coroutine so that the thing that looks like an inner event loop isn't, but rather is just parking things until the condition is satisfied. That option is... rather more difficult to do in Python as the language implementation isn't fully non-recursive (and I'm not sure that Tkinter is set up to handle the mess of async function coloring). Fortunately, provided you're careful, it's not too difficult.
It helps if you know that wait_window is waiting for a <Destroy> event where the target window is the toplevel (and not one of the inner components) and that destroying the main window will trigger it as all the other windows are also destroyed when you do that. In short, as long as you avoid reentrancy you'll be fine with it. You just need to arrange for the button that was clicked to be disabled while the wait is ongoing; that's good from a UX perspective too (the user can't do it, so don't provide the visual hint that they can).
def w(seconds, button):
dummy = tk.Toplevel(root)
dummy.title(seconds)
dummy.after(seconds*1000, lambda x=dummy: x.destroy())
button["state"] = "disabled" # <<< This, before the wait
dummy.wait_window(dummy)
button["state"] = "normal" # <<< This, after the wait
print(seconds)
btn1 = tk.Button(root, text = '5 seconds')
# Have to set the command after creation to bind the button handle to the callback
btn1["command"] = (lambda : w(5, btn1))
This all omits little things like error handling.

Related

Unable to exit tkinter app when using "wait_variable()"

I have a python code that includes tkinter window and other running tasks.
I've been trying to bind "WM_DELETE_WINDOW" event to a function that exits my python code when I close the window but can't achieve that.
This is what I try:
def on_exit():
root.destroy()
sys.exit()
root.protocol('WM_DELETE_WINDOW', on_exit)
The window is destroyed successfully but the python code doesn't exit. Any possible reason for sys.exit() not to work?
What am I doing wrong? any alternative approach should I try?
Doing some testing I figured out what can be the problem.
Here's a small code that summarizes my code which is much bigger.
import tkinter as tk
import sys
root = tk.Tk()
submitted = tk.IntVar()
def on_exit():
root.destroy()
sys.exit()
root.protocol('WM_DELETE_WINDOW', on_exit)
def submit():
submitted.set(1)
print("submitted")
button= tk.Button(root, text="Submit",command=submit)
button.pack()
button.wait_variable(submitted)
root.mainloop()
I believe now that wait_variable is the source of the problem.
And the code actually exits when I added submitted.set(1) to on_exit() ( or if I clicked the button first before closing the window ) but if I tried closing the window without pressing the button, the code won't exit.
So does this mean that wait_variable not only makes tkinter app wait, but also prevents python code exiting?!
I tried os._exit(1) and it worked, but I think it's not clean.
As your updated question points out the problem is wait_variable(). Going off the documentation for this method wait_variable() enters a local event loop that wont interrupt the mainloop however it appears that until that local event loop is terminated (the variable is updated in some way) it will prevent the python instance from terminating as there is still an active loop. So in order to prevent this you have also correctly pointed out you need to update this variable right before you terminate the tk instance.
This might seam odd but it is the behavior I would expect. It is my understanding that an active loop needs to be terminated before a python instance can exit.
As Bryan has pointed out in the comments the wait_variable() method is "a function which calls the vwait command inside the embedded tcl interpreter. This tcl interpreter knows nothing about python exceptions which is likely why it doesn't recognize the python exception raised by sys.exit()"
Link to relevant documentation:
wait_variable()
Relevant text from link:
wait_variable(name)
Waits for the given Tkinter variable to
change. This method enters a local event loop, so other parts of the
application will still be responsive. The local event loop is
terminated when the variable is updated (setting it to it’s current
value also counts).
You can also set the variable to whatever it is currently set as to terminate this event loop.
This line should work for you:
submitted.set(submitted.get())
That said you do not actually need sys.exit(). You can simply use root.destroy().
You new function should look like this:
def on_exit():
submitted.set(submitted.get())
root.destroy()
The python instance will automatically close if there is no more code after the mainloop.

Pause function execution while awaiting input in entry (tkinter)

Let's say I've got a function that requests input. How can I pause the function that calls this function while I am waiting for user input in an entry widget. I tried it with a while loop and time.sleep(sec). Furthermore I executed the function within another thread (so the main thread should not be interrupted), but the problem that always occurs is that the whole program freezes (typing in entry impossible)!
Because I do not have that much experience with Python I am truly stuck.
PS: I am coding on a mac.
The code I used:
import time
_input = []
def get_input()
try:
return _input[0]
except IndexError:
return None
def req():
while get_input() == None:
time.sleep(1)
return get_input()
The function req() is always called within a function which is called via 'getattr()' in a function which parses the input in the entry widget. The variable '_input' automatically gets the user input from the entry. The input I then successfully got from the '_input' variable is then discarded.
Maybe the problem is that the function is running and that is why another function cannot be executed... but shouldn't that be irrelevant if I was using a distinct thread? Why didn't that work...?
Here's a function that creates a simple Tkinter GUI that allows the user to input data into an Entry widget. When the user hits Enter the function gets the current value from the Entry, closes the GUI and returns the value to the calling code. The calling code will block until tkinter_input returns. If the user closes the GUI window with the close button the contents of the Entry are ignored, and None is returned.
import tkinter as tk
def tkinter_input(prompt=""):
root = tk.Tk()
tk.Label(root, text=prompt).pack()
entry = tk.Entry(root)
entry.pack()
result = None
def callback(event):
nonlocal result
result = entry.get()
root.destroy()
entry.bind("<Return>", callback)
root.mainloop()
return result
result = tkinter_input("Enter data")
print(result)
The way to wait for user input is to open up a dialog. A modal dialog will force the user to dismiss the dialog, a non-modal will allow the user to continue to use the main application.
In your case, you can create a dialog using a Toplevel and fill it with any widgets that you want, then use the wait_window function to wait for that window to be destroyed. To make it modal you can create a "grab" on the toplevel. To keep this simple, I have not done that in the following example.
Here is a basic example. The key is the call to wait_window which will not return until the dialog is destroyed.
import tkinter as tk
class CustomDialog(object):
def __init__(self, parent, prompt="", default=""):
self.popup = tk.Toplevel(parent)
self.popup.title(prompt)
self.popup.transient(parent)
self.var = tk.StringVar(value=default)
label = tk.Label(self.popup, text=prompt)
entry = tk.Entry(self.popup, textvariable=self.var)
buttons = tk.Frame(self.popup)
buttons.pack(side="bottom", fill="x")
label.pack(side="top", fill="x", padx=20, pady=10)
entry.pack(side="top", fill="x", padx=20, pady=10)
ok = tk.Button(buttons, text="Ok", command=self.popup.destroy)
ok.pack(side="top")
self.entry = entry
def show(self):
self.entry.focus_force()
root.wait_window(self.popup)
return self.var.get()
To use it, call the show method:
dialog = CustomDialog(root, prompt="Enter your name:")
result = dialog.show()
With the above, result will have the string that you entered.
For more information about creating dialogs, see Dialog Windows on the effbot site,
GUI programming is quite different from normal python scripts.
When you see the GUI pop up, it is already running in the mainloop. That means that your code is only invoked from the mainloop as a callback attached to some event or as a timeout function. Your code actually interrupts the flow of events in the mainloop.
So to keep the GUI responsive, callbacks and timeouts have to finish quickly (say in 0.1 second max). This is why you should not run long loops in a callback; the GUI will freeze.
So the canonical way to do a long calculation in a GUI program is to split it up into small pieces. Instead of e.g. looping over a long list of items in a for loop, you create a global variable that holds the current position in the list. You then create a timeout function (scheduled for running by the after method) that takes e.g. the next 10 items from the list, processes them, updates the current position and reschedules itself using after.
The proper way to get input for a function is to get the necessary input before starting the function. Alternatively, you could use a messagebox in the function to get the input. But in general it is considered good design to keep the "guts" of your program separate from the GUI. (Consider that you might want to switch from Tkinter to the GTK+ or QT toolkits in the future.)
Now onto threads. You might think that using threads can make long-running tasks easier. But that is not necessarily the case. For one thing, the standard Python implementation (we shall call it CPython) has a Global Interpreter Lock that ensures that only one thread at a time can be running Python bytecode. So every time your long-running calculation thread is running, the other thread containing the mainloop is halted. In Python 3 the thread scheduling is improved w.r.t. Python 2 as to try and not starve threads. But there is no guarantee that the GUI thread gets enough runtime when the other thread is doing a ton of work.
Another restriction is that the Tkinter GUI toolkit is not thread safe. So the second thread should not use Tkinter calls. It will have to communicate with the GUI thread by e.g. setting variables or using semaphores. Furthermore, data structures that are used by both threads might have to be protected by Locks, especially if both threads try to modify them.
In short, using threads is not as simple as it seems. Also multithreaded programs are notoriously difficult to debug.

Tkinter Frame Not Recognizing Keypresses

This question is NOT a duplicate of this question: Why doesn't the .bind() method work with a frame widget in Tkinter?
As you can see, I set the focus to the current frame in my game_frame() method.
I'm writing a Chip-8 emulator in Python and using Tkinter for my GUI. The emulator is running, but I can't get Tkinter to recognize keypresses. Here is my code:
def game_frame(self):
self.screen = Frame(self.emulator_frame, width=640, height=320)
self.screen.focus_set()
self.canvas = Canvas(self.screen, width=640, height=320, bg="black")
self._root.bind("<KeyPress-A>", self.hello)
for key in self.CPU.KEY_MAP.keys():
print(key)
self.screen.bind(key, self.hello)
self.screen.pack()
self.canvas.pack()
def hello(self, event):
if event.keysym in self.CPU.KEY_MAP.keys():
self.CPU.keypad[self.CPU.KEY_MAP[event.keysym]] = 1
self.CPU.key_pressed = True
self.CPU.pc += 2
sys.exit()
def run_game(self, event):
self.game_frame()
self.CPU.load_rom("TANK")
while True:
self._root.update()
self.after(0, self.CPU.emulate_cycle)
Could you please help me figure out what's going wrong? I think it might have something to do with my game loop interfering with the key bindings, but I'm not sure. The hello method never gets called when I run the game because the program continues to run in an infinite loop and never exits, regardless of what key is pressed. Thank you!
The problem could be due to two things. Without seeing all your code it's impossible to say for sure.
For one, you are binding to a capital "A" rather than a lowercase "a" -- have you testing that the binding works or not when you press a capital A?
Also, you are using after and update incorrectly. You may be starving the event loop, preventing it from processing key presses. The right way to run a function periodically is to have a function that (re)schedules itself.
class CPU_Class():
...
def run_cycle(self):
self.emulate_cycle()
self._root.after(1, self.run_cycle)
Two things to note:
don't use after(0, ...) -- you need to give tkinter at least a ms or so to process other events.
the run_cycle function is responsible for running one cycle, and then scheduling the next cycle to run in the future.
Once you do that, you no longer need your while loop. You can simply call run_cycle once, and that will start the CPU running.

Simple animation with Tkinter Python

I've searched for a simple animation code with Tkinter but I've found very different examples and I can't understand the correct way to write an animation.
Here my working code to display a simple moving circle:
import tkinter as tk
import time
root=tk.Tk()
canvas=tk.Canvas(root,width=400,height=400)
canvas.pack()
circle=canvas.create_oval(50,50,80,80,outline="white",fill="blue")
def redraw():
canvas.after(100,redraw)
canvas.move(circle,5,5)
canvas.update()
canvas.after(100,redraw)
root.mainloop()
In this code I can't correctly understand: how the after method works, where correctly put the update and the move method (before after method ?), is there another way to write an animation code? may you post me another example and comment the code please?
Thanks :)
Calling update
You should not call canvas.update(). As a general rule of thumb you should never call update. For a short essay on why, see this essay written by one of the original developers of the underlying tcl interpreter.
If you take out the call to canvas.update(), you have the proper way to do animation in a tkinter program.
Calling after to start the animation
You don't need to call after immediately before calling root.mainloop(). This works just as well:
...
redraw()
root.mainloop()
The choice to use or not use after in this specific case is dependent on if you want the animation to start immediately (possibly even before the widget is visible) or if you want it to happen after a short delay (possibly after the widget is made visible)
How after works
mainloop is nothing more than an infinite loop that checks the event queue for events. When it finds an event, it pops it off of the list and processes it. after is nothing more than making a request that says "in 100 ms, please add a new event to the queue". When the time limit expires, an event is added to the queue that says, in effect, "run this command". The next time the loop checks for events, it sees this event, pulls it off of the queue, and runs the command.
When you call after from within a method that itself was called by after, you're saying in effect "wait 100ms and do it again", creating an infinite loop. If you put the call to after before moving the object, you're saying "every 100ms run this function". If you put it after you're saying "run this function 100 ms after the last time it was run". The difference is very subtle and usually not perceptible unless your function takes a long time to run.
my code is:
from tkinter import *
import time
tk = Tk()
płótno = Canvas(tk, width=500, height=500)
płótno.pack()
płótno.create_polygon(10,10,10,70,70,10,fill="blue",outline="black")
for x in range(0,51):
płótno.move(1,5,0)
płótno.update()
rest(0.05)
płótno means canvas

Creating Toplevel widgets that are thread safe

I'm trying to learn how to use the thread module. I followed along with the instructions here: http://effbot.org/zone/tkinter-threads.htm
My hope is the test script will:
Print out the "count" every two seconds
Show a pop-up dialog window (also every 2 seconds)
The pop-ups should be allowed to accumulate (if I don't click "OK" for a while, there should be
multiple pop-ups)
However, when I run this script it will freeze the main window and after a while crash. I think I'm not implementing the thread module correctly.
Could someone please have a look and point out what I'm doing wrong?
Here is what I've tried so far:
from Tkinter import *
import thread
import Queue
import time
class TestApp:
def __init__(self, parent):
self.super_Parent = parent
self.main_container = Frame(parent)
self.main_container.pack()
self.top_frame = Frame(self.main_container)
self.top_frame.pack(side=TOP)
self.bottom_frame = Frame(self.main_container)
self.bottom_frame.pack(side=TOP)
self.text_box = Text(self.top_frame)
self.text_box.config(height=20, width=20)
self.text_box.pack()
self.queue = Queue.Queue()
self.update_me()
def show_popup(self):
self.my_popup = Toplevel(self.main_container)
self.my_popup.geometry('100x100')
self.popup_label = Label(self.my_popup, text="Hello!")
self.popup_label.pack(side=TOP)
self.pop_button = Button(self.my_popup, text="OK", command=self.my_popup.destroy)
self.pop_button.pack(side=TOP)
def write(self, line):
self.queue.put(line)
def update_me(self):
try:
while 1:
line = self.queue.get_nowait()
if line is None:
self.text_box.delete(1.0, END)
else:
self.text_box.insert(END, str(line))
self.text_box.see(END)
self.text_box.update_idletasks()
except Queue.Empty:
pass
self.text_box.after(100, self.update_me)
def pipeToWidget(input, widget):
widget.write(input)
def start_thread():
thread.start_new(start_test, (widget,))
def start_test(widget):
count = 0
while True:
pipeToWidget(str(count) + "\n", widget)
count += 1
time.sleep(2)
widget.show_popup()
root = Tk()
widget = TestApp(root)
start_button = Button(widget.bottom_frame, command=start_thread)
start_button.configure(text="Start Test")
start_button.pack(side=LEFT)
root.title("Testing Thread Module")
root.mainloop()
I can't reproduce your problem, but I can see why it would happen.
You're using the queue to pass messages from the background thread to the main thread for updating text_box, which is correct. But you're also calling widget.show_popup() from the background thread, which means it creates and displays a new Toplevel in the background thread. That's not correct.
All UI code must run in the same thread—not all UI code for each top-level window, all UI code period. On some platforms, you may get away with running each window in its own thread (or even free-threading everything), but that isn't supposed to work, and definitely will crash or do improper things on some platforms. (Also, that single UI thread has to be the initial thread on some platforms, but that isn't relevant here.)
So, to fix this, you need to do the same dance for creating the popups that you do for updating the textbox.
The obvious way to do that is to move the widget.show_popup() to the loop in update_me(). If you want it to happen 2 seconds after the textbox updates, just add self.top_frame.after(2000, self.show_popup) to the method.
But I'm guessing you're trying to teach yourself how to have multiple independent updating mechanisms, so telling you "just use a single update queue for everything" may not be a good answer. In that case, just create two queues, and a separate update method servicing each queue. Then, do your pipeToWidget, sleep 2 seconds, then pipeToPopup.
Another way around this is to use mtTkinter. It basically does exactly what you're doing, but makes it automatic, pushing each actual Tk GUI call onto a queue to be run later by the main loop. Of course your objects themselves have to be thread-safe, and this also means that you have to deal with the GUI calls from one thread getting interleaved with calls from another thread. But as long as neither of those is a problem (and they don't seem to be in your case), it's like magic.
If you want to know why this is freezing and/or crashing for you on Win7 and not for me on OS X 10.8… well, you really need to look into a mess of Tcl, C, and Python code, and also at how each thing is built. And, unless it's something simple (like your Tk build isn't free-threaded), it wouldn't tell you much anyway. The code isn't supposed to work, and if it seems to work for me… that probably just means it would work every time until the most important demo of my career, at which point it would fail.

Categories

Resources