How is tkinter code executed? - python

I am writing a program using tkinter, but I do not understand how it works. Normally, code is executed top-down, but with tkinter it obviously does not.
For example, I have bound a function to the left mouse button, and this function is executed every time I click the button. But how is the other code around that treated? My problem is that I in the start of my program initialize a variable that is used as an argument in the bound function, and then it is changed in the function and returned. But every time the function is called, the variable seems to be reset to its initial value.
Does anyone know why this is?
I have it written like this:
var = "black"
var = c.bind("<Button-1>", lambda event: func(event, arg=var))
The function "func" changes var and returns it, but the next time I press the button the variable is always "black".
Thanks in advance!

Tkinter does indeed run top down. What makes tkinter different is what happens when it gets to the bottom.
Typically, the last executable statement in a tkinter program is a call to the mainloop method of the root window. Roughtly speaking, tkinter programs look like this:
# top of the program logic
root = tkinter.Tk()
...
def some_function(): ...
...
some_widget.bind("<1>", some_function)
...
# bottom of the program logic
root.mainloop()
mainloop is just a relatively simple infinite loop. You can think of it as having the following structure:
while the_window_has_not_been_destroyed():
event = wait_for_next_event()
process_event(event)
The program is in a constant state of waiting. It waits for an event such as a button click or key click, and then processes that event. Conceptually, it processes the event by scanning a table to find if that event has been associated with the widget that caught the event. If it finds a match, it runs the command that is bound to that widget+event combination.
When you set up a binding or associate a command with a button, you are adding something to that table. You are telling tkinter "if event X happens on widget Y, run function Z".
You can't use a return result because it's not your code that is calling this function. The code that calls the function is mainloop, and it doesn't care what the function returns. Anything that gets returned is simply ignored.

Related

How to queue a task AFTER all widget resizing operations?

I want to query a widget's size after changing its contents. Here's a demonstration:
import tkinter as tk
win = tk.Tk()
label = tk.Label(win)
label.pack()
def callback1():
label['text'] = 'hello world'
win.after_idle(callback2)
def callback2():
print('label width:', label.winfo_width())
win.after(0, callback1)
win.mainloop()
According to the documentation, callbacks queued with after_idle should only be executed when there's nothing else to do:
Registers a callback that is called when the system is idle. The callback will be called there are no more events to process in the mainloop.
And yet, callback2 is clearly executed before the label is resized, because the output of the program is this:
label width: 1
Even adding a call to update_idletasks() doesn't change this output. Only if win.update_idletasks() is called in both callback1 and callback2, the correct size is printed. I really don't understand why it's necessary to call it twice.
Question
Why is callback2 being executed before the label is resized? How can I ensure that label.winfo_width() returns the correct size?
Limitations
The main goal of this question is to understand how/when tkinter executes (idle) tasks. I want to find out how to correctly queue tasks so that they're executed only after the GUI has updated itself. I'm not really interested in workarounds such as these:
I'd prefer to avoid using update() because I don't understand how it causes race conditions or when it's safe to use. (In my real code, all of this would be executed inside an event handler, which the documentation explicitly states should be avoided.)
I also want to avoid using the <Configure> event. This is because there might be an arbitrary number of widgets changing size, and I cannot reasonably be expected to bind event handlers to all of their <Configure> events. I really just need a way to execute a callback function after all the resizing has taken place.

Problems with update and update_idletasks

I've been learning python for a month now and run into my first brick wall. I have a large art viewer GUI program and at one point want to put an image on screen with a countdown counter-approx every 5 secs. I thought of a code such as the one below The problem is that this uses update and all my reading says that update is bad (starts a new event loop (?)) and that I should use update_idletasks. when I replace update with update_idletasks in the code below the countdown button is not visible until it reaches single figures, update superficially works fine. But also the q bound key calls the subroutine but has no effect
from tkinter import *
import sys
import time
root = Tk()
def q_key(event):
sys.exit()
frame=Frame(root, padx=100, pady=100, bd=10, relief=FLAT)
frame.pack()
button=Button(frame,relief="flat",bg="grey",fg="white",font="-size 18",text="60")
button.pack()
root.bind("q",q_key)
for x in range(30, -1, -5) :
button.configure(text=str(x))
button.update()
print(x)
button.after(5000)
root.mainloop()
In this case you don't need update nor update_idletasks. You also don't need the loop, because tkinter is already running in a loop: mainloop.
Instead, move the body of the loop to a function, and call the function via after. What happens is that you do whatever work you want to do, and then schedule your function to run again after a delay. Since your function exits, tkinter returns to the event loop and is able to process events as normal. When the delay is up, tkinter calls your function and the whole process starts over again.
It looks something like this:
def show(x):
button.configure(text=x)
if x > 0:
button.after(5000, show, x-5)
show(30)

Python/tkinter: Change label twice with one command execution

Let's say we have three tkinter widgets: a label, a treeview and an Optionmenu (will be called 'menu' for short below). I successfully make the menu execute a function once an option is chosen. The brief function looks like this:
def data_process():
# do something takes time
def print_data()
data_process() # do something takes time too
# print stuff to treeview
def refresh_table(self, args):
label['text'] = 'Executing' # change label text to Executing
print_data() # a func. which takes time to run
label['text'] = 'Done' # change text to Done
label = tk.Label(parent, text = 'ready')
label.pack()
menu = tk.OptionMenu(parent, var, *list, command = lambda _:refresh_table(self, args))
menu.pack()
table = tk.Treeview(parent)
table.pack()
Function print_data is going to print something to a treeview widget (table). The label widget is like a status bar, telling users what's going on now. So the workflow that I attempt to do is:
Select an option in menu and call refresh_table.
Change label text to 'Executing'.
Execute print_data and print stuff in treeview.
When print_data is done, change the label to 'Done'.
Here is the thing. When I select the option, the program stocks (as expected) to do stuff. However, the label isn't changed to 'Executing' at the beginning. Instead, it's changed to 'Done' when the print_data is done executing (almost simultaneously). I suspect that the command refresh_table effects the target widgets once all demands are done. Because I does see the label flashes out 'Executing' but immediately shows 'Done'. Are there any thoughts about this situation? Any suggestion is appreciated. Thank you!
I suspect that the command refresh_table effects the target widgets once all demands are done.
That's true. Your GUI will update every time the mainloop is entered. While your function refresh_table is running, no events (like updating your label) will be processed, since those only occur during idle times (hence their other name, "idle tasks"). But because the UI isn't idle during the execution of refresh_table, the event loop is not accessible until all code in the function is done (because your whole program runs on the same thread) and the redrawing event is pending. You can fix this by calling update_idletasks, which will process all pending events immediately. Would be done like this (assuming that your main window is called parent and you are inside a class definition, the important thing is that you call update_idletasks on your main window directly after changin the label's text):
def refresh_table(self, args):
label['text'] = 'Executing'
self.parent.update_idletasks()
print_data()
label['text'] = 'Done'
There's also update, which will not only call pending idle tasks, but rather process basically everything that there is to process, which is unnecessary in your case. A very nice explanation why update_idletasks is mostly preferable to update can be found in the answer to this question.

Python Tkinter - How do I change variables from outside the main window class?

Now i understand the concept of instance variables and classes, I've never had a problem with them before and I use them frequently. However when I make my MainWindow class, everything is peachy until i try accessing instance variables.
http://pastebin.com/tDs5EJhi is the full code, but at this point it's just placing labels and frames and whatnot, no actual logic is going on. The window looks fine and nothing bad happens.
My question comes to be when I try changing things inside of the window externally. I figured I could just make an instance of the class and change variables from there (namely instancevariable.ImageCanvas.itemconfig()) like i can normally, but Tkinter isn't being nice about it and I think it's a result of Tkinter's mainloop().
Here's the tidbit of my class MainWindow() that i'm having trouble with (ln 207)
...
self.C4 = Tk.PhotoImage(file="temp.png")
self.card4 = self.CardCanvas.create_image(120,46,image=self.C4, state=Tk.NORMAL)
#self.CardCanvas.itemconfig(4, state=Tk.HIDDEN) # < It works here
...
self.root.mainloop()
window = MainWindow()
window.CardCanvas.itemconfig(4, state=Tk.HIDDEN) # < It doesn't work here
That's how i learned how to edit instance variables. When the window pops up, the itemconfig command doesn't actually apply like it would were it inside the class (or maybe it did and the window just didn't update?) and after closing the window I get this error:
_tkinter.TclError: invalid command name
which I assume is just because it's trying to apply a method to variables that don't exist anymore, now that the window has closed.
So I guess here's my big question - I have a MainWindow class, and from what I can tell, nothing can be changed from outside of the class because the Tk.mainloop() is running and won't stop to let other code after it run, like the itemconfig. How do I go about changing those variables? Code after the instance variable declaration doesn't seem to run until the MainWindow() is closed.
You are correct that code after mainloop doesn't run. It does, but only after the GUI has been destroyed. Tkinter is designed for the call to mainloop be the last (or very nearly last) line of executable code. Once it is called, all other work must be done as reaction to events. That is the essence of GUI programming.
The answer to "how do I go about changing the variables" is simple: do it before you call mainloop, or do it in reaction to an event. For example, do it in a callback to a button, do it in a function bound to an event, or to a time-based event via after, and so on.

GUI wait until BooleanVar() changes

how can I make my Tkinter GUI wait for a change of a BooleanVar()? The BooleanVar is controlled by a distance sensor. The GUI should wait until the variable changes to False and the move on.
I tried to use a while True - loop, but as expected it disturbed the mainloop and the programm crashed.
I've also considered to use one if the methods to wait for user-input, but I can't figure out how.
Is there any way to solve this?
Thanks!
I don't understand what you mean by "wait" here, because a GUI is always in a constant state of "wait". It waits for events, and then it acts on events.
If you have a BooleanVar that is set somehow, you can set a trace on that variable. When the value changes, the trace will call a callback of your choice. In that callback your code can do whatever you want.
self.sensor = tk.BooleanVar()
self.sensor.trace("w", self.on_sensor_change)
...
def on_sensor_change(self, *args):
print "the sensor changed:", self.sensor.get()

Categories