Allan S
Allan S

Reputation: 1741

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

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.

Upvotes: 163

Views: 233234

Answers (6)

Llama Del Rae
Llama Del Rae

Reputation: 81

It's a super common issue for first time tkinter programmers and novice python programmers to either get stuck here and give up or implement their code in a way that "works" but is wrong and causes issues. I know because I'm a novice and only just got over this hurdle.

Firstly, read some excellent documentation on how the mainloop() actually works in tkinter which also explains why putting root.update() in your code loop is a terrible idea, especially for longer running scripts (think 24x7 monitoring tools etc) and will cause them to eventually break.

Next, read all up about threading and classes and then try and implement threading only to realise that threading isn't really the architecture that tkinter wants to live in and also that tkinter isn't threadsafe no matter what the doco says.

So, as a novice who just needs a concrete example to get started, please see the following most basic code (no classes, no fluff, cut & paste into your python IDE and press the Go button):

import tkinter as tk

def main(args):
    
    # Build the UI for the user
    root = tk.Tk()
    buttonShutDown = tk.Button(root, text="PUSH ME!")#, command=sys.exit(0))
    buttonShutDown.pack()
    
    # Go into the method that looks at inputs and outputs. Must be run BEFORE you get to root.mainloop()
    monitorInputsAndActionOutputs(root)
    
    # The main tkinter loop. This loop is blocking - ie once you call it your python script stays here forever, hence why you can't do other stuff in your while(blah) loop that you also want to run
    root.mainloop()

def monitorInputsAndActionOutputs(root): 
    
    # This is where your code does something that you want to do regularly and not have blocked by the root.mainloop(). You could make this big or small as you desire.
    print ("checked the room temperature")
    print ("adjusted the airconditioning to make comfy")
    
    # This line causes this method to add a "future event" to the root.mainloop() queue in a manner of speaking. This means that the root.mainloop() method in which your script will be stuck forever will in approx 100 miliseconds time execute this method again for you.
    # Because this method contains this "put myself back in the queue to be run again" trigger, this method will run every 100 miliseconds until you quit your GUI with root.destroy() or sys.exit() or something similar.
    root.after(100, monitorInputsAndActionOutputs, root) 

    return None

# Don't worry about this bit it's not related to your GUI / loop
if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))

This is a super basic example of a air conditioning monitoring script with a GUI with a single button on it - maybe the button enables or disables the AC monitoring system or something else.

Anyway, you get the idea....this piece of code is the architectually appropriate way to implement tkinter WITH some sort of additional running loop that doesn't require the user to interact with the GUI and press a button or similar.

This example

  1. Does not implement threads
  2. Is thread safe (assuming you don't implement threads yourself somewhere else in your code)
  3. Is non blocking (if you put your stuff in that monitorInputsAndActionsOutputs() method
  4. Will result in your script not crashing after an random amount of time
  5. Is novice programmer friendly (no classes, no difficult to understand self architecture)
  6. Scalable - you can add 100 extra methods with the same after() style callback and it will just work assuming you have enough horsepower in your processor - maybe dont put 1k methods with after() in them and then try and run it on a 5 year old Raspberry Pi while watching a high def video

What this code is NOT:

  1. The best implmentation (see the above links)
  2. Easy to manage at scale

But, it's probably useful enough to get a novice on the right path and to help them not fall into the root.update() trap that I did.

Upvotes: 1

Kevin
Kevin

Reputation: 939

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)

Upvotes: 75

Micheal Morrow
Micheal Morrow

Reputation: 95

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()

Upvotes: 4

jma
jma

Reputation: 828

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()  

Upvotes: 29

Dave Ray
Dave Ray

Reputation: 40005

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."""

Upvotes: 191

Bjorn
Bjorn

Reputation:

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.

Upvotes: 9

Related Questions