How do you run your own code alongside Tkinter's event loop?

192,435

Solution 1

Use the after method on the Tk object:

from tkinter import *

root = Tk()

def task():
    print("hello")
    root.after(2000, task)  # reschedule event in 2 seconds

root.after(2000, task)
root.mainloop()

Here's the declaration and documentation for the after method:

def after(self, ms, func=None, *args):
    """Call function once after given time.

    MS specifies the time in milliseconds. FUNC gives the
    function which shall be called. Additional parameters
    are given as parameters to the function call.  Return
    identifier to cancel scheduling with after_cancel."""

Solution 2

The solution posted by Bjorn results in a "RuntimeError: Calling Tcl from different appartment" message on my computer (RedHat Enterprise 5, python 2.6.1). Bjorn might not have gotten this message, since, according to one place I checked, mishandling threading with Tkinter is unpredictable and platform-dependent.

The problem seems to be that app.start() counts as a reference to Tk, since app contains Tk elements. I fixed this by replacing app.start() with a self.start() inside __init__. I also made it so that all Tk references are either inside the function that calls mainloop() or are inside functions that are called by the function that calls mainloop() (this is apparently critical to avoid the "different apartment" error).

Finally, I added a protocol handler with a callback, since without this the program exits with an error when the Tk window is closed by the user.

The revised code is as follows:

# Run tkinter code in another thread

import tkinter as tk
import threading

class App(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)
        self.start()

    def callback(self):
        self.root.quit()

    def run(self):
        self.root = tk.Tk()
        self.root.protocol("WM_DELETE_WINDOW", self.callback)

        label = tk.Label(self.root, text="Hello World")
        label.pack()

        self.root.mainloop()


app = App()
print('Now we can continue running code while mainloop runs!')

for i in range(100000):
    print(i)

Solution 3

When writing your own loop, as in the simulation (I assume), you need to call the update function which does what the mainloop does: updates the window with your changes, but you do it in your loop.

def task():
   # do something
   root.update()

while 1:
   task()  

Solution 4

Another option is to let tkinter execute on a separate thread. One way of doing it is like this:

import Tkinter
import threading

class MyTkApp(threading.Thread):
    def __init__(self):
        self.root=Tkinter.Tk()
        self.s = Tkinter.StringVar()
        self.s.set('Foo')
        l = Tkinter.Label(self.root,textvariable=self.s)
        l.pack()
        threading.Thread.__init__(self)

    def run(self):
        self.root.mainloop()


app = MyTkApp()
app.start()

# Now the app should be running and the value shown on the label
# can be changed by changing the member variable s.
# Like this:
# app.s.set('Bar')

Be careful though, multithreaded programming is hard and it is really easy to shoot your self in the foot. For example you have to be careful when you change member variables of the sample class above so you don't interrupt with the event loop of Tkinter.

Solution 5

This is the first working version of what will be a GPS reader and data presenter. tkinter is a very fragile thing with way too few error messages. It does not put stuff up and does not tell why much of the time. Very difficult coming from a good WYSIWYG form developer. Anyway, this runs a small routine 10 times a second and presents the information on a form. Took a while to make it happen. When I tried a timer value of 0, the form never came up. My head now hurts! 10 or more times per second is good enough for me. I hope it helps someone else. Mike Morrow

import tkinter as tk
import time

def GetDateTime():
  # Get current date and time in ISO8601
  # https://en.wikipedia.org/wiki/ISO_8601 
  # https://xkcd.com/1179/
  return (time.strftime("%Y%m%d", time.gmtime()),
          time.strftime("%H%M%S", time.gmtime()),
          time.strftime("%Y%m%d", time.localtime()),
          time.strftime("%H%M%S", time.localtime()))

class Application(tk.Frame):

  def __init__(self, master):

    fontsize = 12
    textwidth = 9

    tk.Frame.__init__(self, master)
    self.pack()

    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             text='Local Time').grid(row=0, column=0)
    self.LocalDate = tk.StringVar()
    self.LocalDate.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             textvariable=self.LocalDate).grid(row=0, column=1)

    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             text='Local Date').grid(row=1, column=0)
    self.LocalTime = tk.StringVar()
    self.LocalTime.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             textvariable=self.LocalTime).grid(row=1, column=1)

    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             text='GMT Time').grid(row=2, column=0)
    self.nowGdate = tk.StringVar()
    self.nowGdate.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             textvariable=self.nowGdate).grid(row=2, column=1)

    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             text='GMT Date').grid(row=3, column=0)
    self.nowGtime = tk.StringVar()
    self.nowGtime.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             textvariable=self.nowGtime).grid(row=3, column=1)

    tk.Button(self, text='Exit', width = 10, bg = '#FF8080', command=root.destroy).grid(row=4, columnspan=2)

    self.gettime()
  pass

  def gettime(self):
    gdt, gtm, ldt, ltm = GetDateTime()
    gdt = gdt[0:4] + '/' + gdt[4:6] + '/' + gdt[6:8]
    gtm = gtm[0:2] + ':' + gtm[2:4] + ':' + gtm[4:6] + ' Z'  
    ldt = ldt[0:4] + '/' + ldt[4:6] + '/' + ldt[6:8]
    ltm = ltm[0:2] + ':' + ltm[2:4] + ':' + ltm[4:6]  
    self.nowGtime.set(gdt)
    self.nowGdate.set(gtm)
    self.LocalTime.set(ldt)
    self.LocalDate.set(ltm)

    self.after(100, self.gettime)
   #print (ltm)  # Prove it is running this and the external code, too.
  pass

root = tk.Tk()
root.wm_title('Temp Converter')
app = Application(master=root)

w = 200 # width for the Tk root
h = 125 # height for the Tk root

# get display screen width and height
ws = root.winfo_screenwidth()  # width of the screen
hs = root.winfo_screenheight() # height of the screen

# calculate x and y coordinates for positioning the Tk root window

#centered
#x = (ws/2) - (w/2)
#y = (hs/2) - (h/2)

#right bottom corner (misfires in Win10 putting it too low. OK in Ubuntu)
x = ws - w
y = hs - h - 35  # -35 fixes it, more or less, for Win10

#set the dimensions of the screen and where it is placed
root.geometry('%dx%d+%d+%d' % (w, h, x, y))

root.mainloop()
Share:
192,435
someGuy
Author by

someGuy

Updated on April 02, 2020

Comments

  • someGuy
    someGuy about 4 years

    My little brother is just getting into programming, and for his Science Fair project, he's doing a simulation of a flock of birds in the sky. He's gotten most of his code written, and it works nicely, but the birds need to move every moment.

    Tkinter, however, hogs the time for its own event loop, and so his code won't run. Doing root.mainloop() runs, runs, and keeps running, and the only thing it runs is the event handlers.

    Is there a way to have his code run alongside the mainloop (without multithreading, it's confusing and this should be kept simple), and if so, what is it?

    Right now, he came up with an ugly hack, tying his move() function to <b1-motion>, so that as long as he holds the button down and wiggles the mouse, it works. But there's got to be a better way.

  • Nathan
    Nathan almost 15 years
    if you specify the timeout to be 0, task will put itself back on the event loop immediately after finishing. this will not block other events, while still running your code as often as possible.
  • Russell Smith
    Russell Smith over 13 years
    You have to be very careful with this kind of programming. If any events cause task to be called you'll end up with nested event loops, and that's bad. Unless you fully understand how event loops work you should avoid calling update at all costs.
  • jldupont
    jldupont almost 13 years
    I used this technique once - works OK but depending on how you do it, you might have some staggering in the UI.
  • jldupont
    jldupont almost 13 years
    Not sure this can work. Just tried something similar and I get "RuntimeError: main thread is not in main loop".
  • mgiuca
    mgiuca almost 12 years
    jldupont: I got "RuntimeError: Calling Tcl from different appartment" (possibly the same error in a different version). The fix was to initialise Tk in run(), not in __init__(). This means that you are initialising Tk in the same thread as you call mainloop() in.
  • Andre Holzner
    Andre Holzner over 8 years
    typically you would pass arguments to __init__(..), store them in self and use them in run(..)
  • JxAxMxIxN
    JxAxMxIxN over 7 years
    After pulling my hair out for hours trying to get opencv and tkinter to work together properly and cleanly close when the [X] button was clicked, this along with win32gui.FindWindow(None, 'window title') did the trick! I'm such a noob ;-)
  • Anonymous
    Anonymous about 5 years
    This is not the best option; although it works in this case, it is not good for most scripts (it only runs every 2 seconds), and setting the timeout to be 0, per the suggestion posted by @Nathan because it only runs when tkinter is not busy (which could cause problems in some complex programs). Best to stick with the threading module.
  • Bob Bobster
    Bob Bobster almost 5 years
    The root doesn't show up at all, giving the warning: ` WARNING: NSWindow drag regions should only be invalidated on the Main Thread! This will throw an exception in the future `
  • m3nda
    m3nda over 4 years
    This is a life saver. Code outside the GUI should be checking for tkinter thread to be alive if you wan't to be able to exit the python script once exiting the gui. Something like while app.is_alive(): etc
  • Green05
    Green05 almost 4 years
    @Bryan Oakley Is update a loop then? And how would that be problematic?
  • Jason Waltz
    Jason Waltz almost 4 years
    Wow, I have spent hours now debugging why my gui kept freezing. I feel stupid, thanks a million!
  • FotisK
    FotisK over 3 years
    How would you implement to be able to update the label text when ever it is needed from within the for loop or from the rest of the script?
  • John Sohn
    John Sohn almost 3 years
    i was trying to make this work with a window defined outside the thread code so it could be accessed by the other code.
  • weeix
    weeix over 2 years
    If your task() is CPU intensive, threading solutions (e.g. posted by Kevin and Bjorn) might be needed. I originally use after() for my opencv task because it seems simple, resulting in a painfully slow GUI --- just resizing the window took about 2-3 seconds.
  • Amnon Harel
    Amnon Harel over 2 years
    Great answer! As is often the case with asynchronous code, and tkinter in particular, the tricky part is in exiting cleanly. The code above kept the window displayed in my Ubuntu system. Using self.root.destroy() instead of .quit() helped, and so did "del self.root", but it still didn't close cleanly every time. The combination that seems to work is to keep the self.root.quit() and add a "del self.root" after the self.root.mainloop().
  • Amnon Harel
    Amnon Harel over 2 years
    BTW: you can communicate between the threads (with the usual pit falls) using global variables and even by issuing tk events from the main thread, a-la stackoverflow.com/questions/270648/…
  • ArduinoBen
    ArduinoBen about 2 years
    Using this for opencv as well, thanks for the heads up!
  • ArduinoBen
    ArduinoBen about 2 years
    Trying to run a similar implementation of this (tkinter + opencv) on my Windows 11 system and ctrl + c doesn't behave nicely with this :/