What magic prevents Tkinter programs from blocking in interactive shell?
Asked Answered
S

1

21

Note: This is somewhat a follow-up on the question: Tkinter - when do I need to call mainloop?

Usually when using Tkinter, you call Tk.mainloop to run the event loop and ensure that events are properly processed and windows remain interactive without blocking.

When using Tkinter from within an interactive shell, running the main loop does not seem necessary. Take this example:

>>> import tkinter
>>> t = tkinter.Tk()

A window will appear, and it will not block: You can interact with it, drag it around, and close it.

So, something in the interactive shell does seem to recognize that a window was created and runs the event loop in the background.

Now for the interesting thing. Take the example from above again, but then in the next prompt (without closing the window), enter anything—without actually executing it (i.e. don’t press enter). For example:

>>> t = tkinter.Tk()
>>> print('Not pressing enter now.') # not executing this

If you now try to interact with the Tk window, you will see that it completely blocks. So the event loop which we thought would be running in the background stopped while we were entering a command to the interactive shell. If we send the entered command, you will see that the event loop continues and whatever we did during the blocking will continue to process.

So the big question is: What is this magic that happens in the interactive shell? What runs the main loop when we are not doing it explicitly? And why does it need to halt when we enter commands (instead of halting when we execute them)?

Note: The above works like this in the command line interpreter, not IDLE. As for IDLE, I assume that the GUI won’t actually tell the underlying interpreter that something has been entered but just keep the input locally around until it’s being executed.

Sotelo answered 2/1, 2014 at 20:56 Comment(2)
This part of the magic is actually really simple—it just weaves the Tcl main loop into the Python main loop. However, the platform-specific stuff that Python does to allow it to wait on stdin and also run a GUI loop (on Windows and classic Mac you have to ask for this manually by running pythonw, on OS X it's what causes an app icon to suddenly appear in your Dock, on most *nix/X11 systems it's more invisible), that's ugly.Hsining
You can see how simple the tkinter—specific thing is: if you just run import tkinter; tk=tkinter.Tk(); input() (as a script, not in the interactive interpreter), Python will start up the combined-GUI-and-stdin loop, which is all it takes to get Tcl running while it's waiting on your input.Hsining
H
16

It's actually not being an interactive interpreter that matters here, but waiting for input on a TTY. You can get the same behavior from a script like this:

import tkinter
t = tkinter.Tk()
input()

(On Windows, you may have to run the script in pythonw.exe instead of python.exe, but otherwise, you don't have to do anything special.)


So, how does it work? Ultimately, the trick comes down to PyOS_InputHook—the same way the readline module works.

If stdin is a TTY, then, each time it tries to fetch a line with input(), various bits of the code module, the built-in REPL, etc., Python calls any installed PyOS_InputHook instead of just reading from stdin.

It's probably easier to understand what readline does: it tries to select on stdin or similar, looping for each new character of input, or every 0.1 seconds, or every signal.

What Tkinter does is similar. It's more complicated because it has to deal with Windows, but on *nix it's doing something pretty similar to readline. Except that it's calling Tcl_DoOneEvent each time through the loop.

And that's the key. Calling Tcl_DoOneEvent repeatedly is exactly the same thing that mainloop does.

(Threads make everything more complicated, of course, but let's assume you haven't created any background threads. In your real code, if you want to create background threads, you'll just have a thread for all the Tkinter stuff that blocks on mainloop anyway, right?)


So, as long as your Python code is spending most of its time blocked on TTY input (as the interactive interpreter usually is), the Tcl interpreter is chugging along and your GUI is responding. If you make the Python interpreter block on something other than TTY input, the Tcl interpreter is not running and the your GUI is not responding.


What if you wanted to do the same thing manually in pure Python code? You'd of need to do that if you want to, e.g., integrate a Tkinter GUI and a select-based network client into a single-threaded app, right?

That's easy: Drive one loop from the other.

You can select with a timeout of 0.02s (the same timeout the default input hook uses), and call t.dooneevent(Tkinter.DONT_WAIT) each time through the loop.

Or, alternatively, you can let Tk drive by calling mainloop, but use after and friends to make sure you call select often enough.

Hsining answered 2/1, 2014 at 21:57 Comment(3)
Thanks a lot for that insight and the references to the source!Sotelo
More generically, what inside of Tcl_DoOneEvent is letting it select on either stdin or GUI events? I see in Tkinter where it handles Tcl events but it never seems to get back to Python. I installed PyOS_InputHook but since the select statement always waits for 100ms the GUI stutters badly.Epner
Could use please give more examples on how to solve this kind of problems (expand your answer), or a good more complete reference with examples?Ranite

© 2022 - 2024 — McMap. All rights reserved.