Search code examples
pythontkintercanvaspython-asynciopython-multithreading

In python, is there a way to set a tkinter update timer that is non blocking, while also keeping the window open?


Here is the code, the question is centered around the mainLoop() function at the bottom. I'm wondering if there is a way for this function to be called in a non-blocking manner on an interval based on the 'fps' variable.

I tried something that should technically work, I imported 'threading' and in the mainLoop() function, I set a threading.Timer to call the mainLoop() function again after 1/fps seconds. The only problem with this is that the window closes right after opening, and I know that if I called tkinter's root.mainloop() function, although the window would stay open, it would block and halt the rest of the code.

(I also tried a while True loop and have time.sleep(1/fps) at the end, but this requires everything to be finished before calling the code again (it is blocking))

Is there a better way to do this, maybe using asyncio or something similar? Doing this in JavaScript would just be a matter of 'setInterval(mainLoop, 1000/fps)'.

#! /usr/bin/python
from tkinter import *
from threading import Timer
import math

# setup window + canvas
root = Tk()
root.title("Pendulum Simulation")
root.resizable(0,0)
canvas = Canvas(root, width = 600, height = 600, bg="grey", bd=0)
canvas.pack()
root.update()

# world variables
fps = 60
gravity = -1250

# Pendulum variables
length = 240
radius = 15
pinX = canvas.winfo_width()/2
pinY = canvas.winfo_height()/2
a = 179.9 # angle to vertical
a *= math.pi/180 # conversion to radians, needed for math
aV = 0 # angular velocity



# two wrapper functions to draw shapes
def drawLine(x1,y1,x2,y2):
    id = canvas.create_line(x1,canvas.winfo_height()-y1,x2,canvas.winfo_height()-y2, width = 2)
def drawCircle(x,y,r,c):
    id = canvas.create_oval(x - r,canvas.winfo_height()-y -r,x+r,canvas.winfo_height()-y+r, fill = c, width = 2)



def drawObjects():
    x = pinX + length*math.sin(a)
    y = pinY - length*math.cos(a)
    drawLine(pinX,pinY, pinX + length*math.sin(a), pinY - length*math.cos(a))
    drawCircle(pinX,pinY,2,"black")
    drawCircle(x,y,radius, "crimson")
    
def moveObjects():
    global aV
    global a
    
    aA = gravity/length * math.sin(a)
    aV += aA/fps
    a += aV/fps


def mainLoop():
    canvas.delete(ALL)
    drawObjects()
    root.update()
    moveObjects()
    Timer(round(1/fps,2), mainLoop).start()

mainLoop()

Solution

  • Tkinter has its own main loop. You can actually hook your own code into that loop, with the after method of your root window. First, which I believe you haven't done, start the mainloop. In your looping function, you can use the after method, after it is called.

    def mainLoop():
         root.after(1000, mainLoop) #Calls mainLoop every 1 second.
         (...)
    mainLoop()
    root.mainloop()
    

    This is non-blocking and shall execute every second.