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.
Related
I have a funtion, that parses the log. I am updating the label color and text at different lines within the same funtion. I see that only the last changed color and text only reflecting in the UI. i do not see the intermediate color and text changes.
Below is my code:
def OnclickLogParser(self):
if LogFileName == '':
messagebox.showinfo("Error", "Please select a valid Log")
if LogPathName == '':
messagebox.showinfo("Error", "Please select a valid Log Path")
self.lb_log_status.configure(bg="#08DFE8", fg="#010101", text='Parsing inProgress...')
m_logParser = CAdpBrrLogParser()
m_logReader = CAdpBrrLogReader('mrr', m_logParser)
status = m_logReader.readFile(LogPathName)
if status == True:
self.lb_log_status.configure(bg="#F6F50B", fg="#010101", text='Log Ready')
self.btn_log_start["state"] = "normal"
global m_injector
m_injector = CAdpUdpDataInjector()
you can see that i am changing the color and text of lb_log_status at two different places. The time gap between those two lines would be around 3 -5 secs. But i can not witness the first color change. '
Tkinter must process events regularly to perform any UI operations. If you want to see the color changes here you have to give Tk a chance to process the configuration and paint events that get generated when you re-configure a widget. Your code just immediately starts processing the log and will not process any events until this function exits and you return to the Tk mainloop() which is the event processing method. You would see the same problem if you used a progress bar widget and tried to update the progress during processing.
One way to resolve this is to use the Tk after() method and schedule your processing in chunks that take a limited amount of time per chunk. After reading and processing a chunk call after() again to schedule the next chunk.
Another method is to put the processing on a worker thread and use event_generate() to post events back to the Tk thread to announce progress and completion.
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.
I'm trying to create simple progress status label for my Pyqt5 Python code and update it after every iteration of a loop in which a function does a bunch of stuff. The label I want to be updated is "status_label_counter". The below code only shows the part for creating labels and the exact place where I want to use the functionality I've mentioned.
#initialisation code, etc...
self.status_label_counter = QLabel()
self.status_label_from = QLabel(' from: ')
self.status_label_total = QLabel()
status_hbox = QHBoxLayout()
status_hbox.addStretch()
status_hbox.addWidget(self.status_label_counter)
status_hbox.addWidget(self.status_label_from)
status_hbox.addWidget(self.status_label_total)
status_hbox.addStretch()
#bunch of other code...
def create_ics(self):
counter = 0
self.status_label_total.setText(str(len(self.groups)))
for group in self.groups:
#does a bunch of stuff inside
group_manager.create_calendar_for(self.rows, group, self.term)
counter += 1
#for console output
print('iteration: ', counter)
#trying to update status counter
self.status_label_counter.setText(str(counter))
The problem is that I only see the update of both labels when the loop is done with the nested function. When I click a button that calls for "create_ics" function window becomes inactive for about 5 seconds, I see logs on console with count of iterations, but nothing happens in view.
The view (Qt) is locked in your main thread and never gets its chance to process its event loop and thus redraw itself. If you really want to do it this way, call:
self.status_label_counter.repaint()
After you set the text (and if you have some complex layout measuring call QApplication.processEvents() instead).
However, much better option would be to run your create_ics() function in a separate thread leaving your main thread to deal with the view and Qt's event processing. You can do it either through standard Python's threading module, or using Qt's own QThread: https://nikolak.com/pyqt-threading-tutorial/ .
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.
When trying to interrupt a tkinter application, it seems that time.sleep() puts some previous command on the hold. According to my understanding and previous experiences, label1's text should be set to "before" for one second and then changed to "after". However, the "before" value never shows up and "after" gets printed normally one second after execution.
from tkinter import *
import time
class Display(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
label1 = Label(text = "before")
label1.grid()
self.after(1000, label1.config(text = "after"))
def main():
root = Tk()
Display(root)
root.mainloop()
if __name__ == '__main__':
main()
Note that using time.sleep(...) yeilds the same result as the tkinter after(...)
from tkinter import *
import time
class Display(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
label1 = Label(text = "before")
label1.grid()
time.sleep(1)
label1.config(text = "after")
def main():
root = Tk()
Display(root)
root.mainloop()
if __name__ == '__main__':
main()
I suppose tkinter is waiting for something to execute the graphical work (console don't have the problem) but I don't see what and the tkinter doc dosen't tackle that issue.
Is there a simple way to get the obvious expected result?
time.sleep(...) does not give the same result as the after(...) call.
The time.sleep method runs, but does not show anything before the second has passed. You can see this by putting print('before') before your sleep call. You'll see the print statement is executed one second before the window is created. This is due to Tkinter not updating anything during a sleep. So nothing happens until the sleep is over, after which the label is immediately updated. Moreover, the mainloop isn't called before the sleep is over, so Tkinter isn't in its mainloop yet. You can force Tkinter to update by using self.parent.update() right before your sleep call. You will see that the window shows, having the label 'before', waits one second and the label changes to 'after'. However, during this second the window is unresponsive, which is why using sleep together with Tkinter is a bad idea.
Instead of sleep, after is almost always the better option since it returns immediately, so it doesn't block further execution, and schedules the specified function to be executed after the specified amount of time. However, after expects a function name to be passed, but you pass a function call. This means that at the time that the after call is evaluated, the function is run and the return value (which is None) is passed as function name. Therefore, you see that the label is changed at the time the window opens, because the change is made at the time the after call is evaluated. What you should do is pass a function name to after:
def update_label:
label1.config(text = "after")
self.after(1000, update_label)
Or shorter, by creating an anonymous function:
self.after(1000, lambda: label1.config(text = "after"))
This will give you the expected result of showing a label with 'before', which changes to 'after' after a second, without blocking Tkinter's mainloop.
It's not surprising that doesn't work. Think about what the Tk framework is doing: it's creating your windows, display = Display(), and at that moment the thread stops. The object is not fully created. Do you expect Tk to render an object that's half way through its constructor?
First, try moving any thread-related code out of the constructor. The constructor should be allowed to finish normally, and thread-code should be in another function.
Second, it's bad practice to use thread.sleep in the middle of your normal logic. Normally, you start a separate thread, and that thread can wait and sleep if it wants to. (Though I don't know whether Tk has a special way of doing things.)