Tkinter after method / threading block - python

I have a strange issue with TKinter after() method. I'm calling function func_a() (blocking call that takes some ms) in main thread and func_b() in after() to read a value at regular interval. It works like a charm, I can get some updated value during func_a() execution
I do not need any graphical interface, so I do not use anymore TKinter, now I'm calling func_a() in main thread. I create a separate thread to call func_b(). The issue is that the call to func_a() stops the execution of func_b() separate thread. I need to wait for func_a() returns to have some periodic call of func_b(). I do not have source of func_a() and func_b() (python C bindings). But maybe some thread locking mechanism prevents func_b() call when func_a() is called.
The question is, what is implemententation behind tkinter after? How can I achieve same behavior as Tkinter after(): be able to call func_b() when func_a()is called, without using TKinter?
Code looks like that :
pos_th= threading.Thread(target=read_pos, args=(0.1,))
pos_th.daemon = True
pos_th_stop = False
pos_th.start()
func_a()
def read_pos(period):
while not pos_th_stop :
func_b()
time.sleep(period)

The question is, what is implemententation behind tkinter after?
When it comes down to it, it's really quite simple. Tkinter's mainloop method is little more than an infinite loop that waits for items to appear on the event queue. When it finds an event, it pulls it off of the queue and calls the handlers for that event.
after simply puts an item on the event queue. During each iteration of mainloop tkinter will examine the timestamp on the function added by after, and if the given amount of time has elapses, the function is pulled off of the queue and run.

Related

Call a function in main thread once threaded function is completed

I am building a Tkinter GUI and I have a process that takes a while to complete, so I threaded it to prevent the GUI from hanging. Lets call the threaded function foo. Once foo is completed, I need to call another function, bar. bar needs to be called from the main thread (it uses a matplotlib method that does not work inside of a thread).
I can't seem to wrap my head around how I might do this. I thought about joining the thread, but that just causes the GUI to hang. I also thought about using a signal variable that I would change in the last line of foo to tell the rest of my program that it is done and its time to execute bar, but then I couldn't figure out how I could continuously check that variable in the main thread without hanging the GUI. Any ideas?
Using Python 3.7
You can use threading.Event() object to notify the main thread and use after() to call a function periodically to check the Event() object to determine when to call bar().
Below is a simple example:
import tkinter as tk
import threading
import time
def foo(event):
print('foo started')
time.sleep(5)
print('foo done')
# notify main thread
event.set()
def bar():
print('hello')
def check_event(event, callback):
print('.', end='')
if event.is_set():
# thread task is completed
callback()
else:
# check again 100 ms (adjust this to suit your case) later
root.after(100, check_event, event, callback)
root = tk.Tk()
# create the `Event()` object
event = threading.Event()
# start the checking
check_event(event, bar)
# start the thread task
threading.Thread(target=foo, args=(event,)).start()
root.mainloop()

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.

Pausing Python Code When Console Output Has Been Re-Routed to GUI

I borrowed a design that I found on stackoverflow to redirect console output to a PyQt5 GUI textEdit widget. This works fine, but the text is not displayed in "real-time". It seems to output the text to the GUI once a process has completed. This has not been a problem until I tried to use time.sleep(secs) to print something, pause, then print something else. What ends up happening is that the program pauses for secs, then it prints all of the statements at once.
This class is in the mainWindow file for the GUI:
class EmittingStream(QtCore.QObject):
textWritten = QtCore.pyqtSignal(str)
def write(self, text):
self.textWritten.emit(str(text))
This is in the __init__ method of my event handling file:
sys.stdout = EmittingStream(textWritten=self.normalOutputWritten)
self.case_setup_console.setReadOnly(True)
self.main_console.setReadOnly(True)
This function is in the main class of event handling file (outside __init__):
def normalOutputWritten(self, text):
"""Append text to the QTextEdit."""
# Maybe QTextEdit.append() works as well, but this is how I do it:
cursor = self.case_setup_console.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(text)
self.case_setup_console.setTextCursor(cursor)
self.case_setup_console.ensureCursorVisible()
This works as intended to re-route the output to the text edit widget self.case_setup_console. But, when I try to run a code such as:
print('This is the first message')
time.sleep(5)
print('This should print 5 seconds later')
What happens is that the program waits 5 seconds, then it prints both statements together.
When programing for GUI code, there is a fundamental shift in how the program is designed. To make it short: after building and initialisation, the program is all the time running in an "event loop" provided by the GUI framework, and your code is only called when specific events take place.
That is in contrast with a terminal application where your code is running all the time, and you tell when to do "print"s, "input"s and pauses with "time.sleep".
The GUI code is responsible for taking notes of events (keyboard, UI, network, etc...), redrawing window contents and calling your code in response to events, or just when it is time to redraw a content that is defined in your code (like updating a background image).
So, it can only render the text that is supposed to show up in a window, with your redirected "print", when the control is passed back to its event loop. When you do time.sleep you pause the return - no code in the event loop is run, and it can't, of course, do any screen drawing.
What is needed is that you write your pauses in the program in a way that during the pause, the GUI event loop is running - not "time.sleep", that just suspends your whole thread.
In Qt the way to do that is create a QTimer object to call the code you want to use to print text at a particular moment, and then just surrender the execution to the the QtMainloop by returning from your function.
Thanks to Python's support for nested functions, that can be done in painless ways, even using lambda functions when setting the timer itself.
...
print('This is the first message')
timer = QtCore.QTimer
timer.singleShot(5000, lambda *_: print('This should print 5 seconds later'))
Should work for the given example. (The call, as usual for UIs, takes the pause time in miliseconds rather than in seconds).
If you will need to schedule more text to be printed after each phrase is output, you will need to call the scheduling inside the callback itself, and will need a little more sophistication, but it still could be as simple as:
phrases = iter(("hello", "world", "I", "am", "talking", "slowly!"))
timer = QtCore.QTimer()
def talker(*_):
phrase = next(phrases, None)
if not phrase:
return
print(phrase)
timer.singleShot(1000, talker)
timer.singleShot(1000, talker)
(Note that there is nothing special about the *_ parameter name either: I am just indicating that there might be any number of positional arguments for the callback - (The "*" part, that is Python syntax) - and that I won't care about then (I call the argument sequence as "_" to indicate I don't care how this is called, as it won't be used anyway - that is a coding convention) )
The iter and next calls are more "Python dialect" than one might be used, but one could just use a list and a counter up to the list length all the same.

How is tkinter code executed?

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.

Tkinter auto-trigger callback continuously

I have a simple tkinter callback that scrubs forward through a video when a key is pressed or held down.
root.bind('<Right>', callback_scrubFwd)
root.mainloop()
This plays back the video very nicely. How can I trigger this callback to be called continuously, which is what happens when the key is held down by the user, only automatically? I've tried ordinary while loops or nested/timed function calls but these lock up the interface.
If you want a function to run continuously, at the end of the function you can call after to put another invocation of the callback on the event queue:
def callback_scrubFwd():
<your code here>
root.after(1000, callback_scrubFwd)
If you want to be able to stop the auto-repeat you can add a flag that you check for each time it is called:
def callback_scrubFwd():
<your code here>
if do_autorepeat:
root.after(1000, callback_scrubFwd)

Categories