Use TkInter without mainloop

I am building a small python program that is waiting for input from a bluetooth device. Depending on the input of the device I want to update my GUI. My decision was TkInter, the de-facto standard GUI for Python. But my main problem was the blocking method mainloop.

The mainloop method is blocking

The method mainloop has an important role for TkInter, it is waiting for events and updating the GUI. But this method is blocking the code after it. You have a conflict, if the core of your application has also a blocking loop that is waiting for some events. In my case waiting for input from a bluetooth device.

A minimal example

Let us assume the following small example with the raw_input instead of bluetooth. We have a minimal TkInter GUI and a while loop that will just close if you type in exit. In all other input cases we want to modify the GUI depending on the user input.

TkInter Hello World!
from Tkinter import *

ROOT = Tk()
LABEL = Label(ROOT, text="Hello, world!")
LABEL.pack()
ROOT.mainloop()
LOOP_ACTIVE = True
while LOOP_ACTIVE:
    USER_INPUT = raw_input("Give me your command! Just type \"exit\" to close: ")
    if USER_INPUT == "exit":
        LOOP_ACTIVE = False
    else:
        LABEL = Label(ROOT, text=USER_INPUT)
        LABEL.pack()

My naive imagination of this program was something like a passive GUI that can be controlled by the bash input. But the result was something else. The GUI opened and the bash was not asking for a command. As I closed the GUI the bash prompted Give me your command! Just type "exit" to close:, I was answering with Hello Gordon!, but the result was error, cause the GUI was already closed.

python tkexample.py
Give me your command! Just type "exit" to close: Hello Gordon!
Traceback (most recent call last):
  File "tkexample.py", line 13, in <module>
    LABEL.pack()
  File "/usr/lib/python2.7/lib-tk/Tkinter.py", line 1939, in pack_configure
    + self._options(cnf, kw))
_tkinter.TclError: can't invoke "pack" command: application has been destroyed

Possible solutions

There are three possible solutions in my eyes, the after method, a thread and using method update in my own loop. All three solutions are mentioned as possible solutions in the following question on stackoverflow.com. I will try all three solutions on the minimal example from above.

Method after

The after method is just taken a delay and a callback method.

from Tkinter import *

ROOT = Tk()

def ask_for_userinput():
    user_input = raw_input("Give me your command! Just type \"exit\" to close: ")
    if user_input == "exit":
        ROOT.quit()
    else:
        label = Label(ROOT, text=user_input)
        label.pack()
        ROOT.after(0, ask_for_userinput)

LABEL = Label(ROOT, text="Hello, world!")
LABEL.pack()
ROOT.after(0, ask_for_userinput)
ROOT.mainloop()

The example with the after method worked pretty well. It was possible to communicate with the bash and to update the GUI with the input of the bash.

python tkexample.py
Give me your command! Just type "exit" to close: Hello Gordon!
Give me your command! Just type "exit" to close: Hello TkInter!
Give me your command! Just type "exit" to close: exit
TkInter Hello World! interactive

Thread

We will use threading.Thread to get the assumed behavior of our small application. The following small application with a thread has the same behavior as the one with the after method from above.

from Tkinter import *
import threading

class App(threading.Thread):

    def __init__(self, tk_root):
        self.root = tk_root
        threading.Thread.__init__(self)
        self.start()

    def run(self):
        loop_active = True
        while loop_active:
            user_input = raw_input("Give me your command! Just type \"exit\" to close: ")
            if user_input == "exit":
                loop_active = False
                self.root.quit()
                self.root.update()
            else:
                label = Label(self.root, text=user_input)
                label.pack()

ROOT = Tk()
APP = App(ROOT)
LABEL = Label(ROOT, text="Hello, world!")
LABEL.pack()
ROOT.mainloop()

Without mainloop

The third solution is just without the mainloop method. You may have seen the update method in the example above. The following solution behaves like the two other solutions. But the one without mainloop is my favourite, cause the difference to the original idea is just one row. But there is a huge disadvantage, the handling of events in TkInter is not working anymore if our own loop is not calling update often enough. In my case no problem, cause all I want is to show some pictures that are only waiting for bluetooth input.

from Tkinter import *

ROOT = Tk()
LABEL = Label(ROOT, text="Hello, world!")
LABEL.pack()
LOOP_ACTIVE = True
while LOOP_ACTIVE:
    ROOT.update()
    USER_INPUT = raw_input("Give me your command! Just type \"exit\" to close: ")
    if USER_INPUT == "exit":
        ROOT.quit()
        LOOP_ACTIVE = False
    else:
        LABEL = Label(ROOT, text=USER_INPUT)
        LABEL.pack()
Next Previous