pythonmultithreadingtkintertimerscale

Tkinter - Sample Scale value every X seconds?


So I have a basic Tkinter GUI with a scale slider. I want to use a timer to sample the value of the scale every 1 second (which should simulate a car accelerator.)

How can I add a timer to my code?

import tkinter as tk
import ttkbootstrap as ttk

SCALE_VALUE_MIN = -20
SCALE_VALUE_MAX = 100
SCALE_VALUE_DEFAULT = 0

MAX_FORWARD_ACC = 3 # m/s^2
MAX_BACKWARD_ACC = MAX_FORWARD_ACC / 3

MAX_FORWARD_SPEED = 220 / 3.6 # 220 km/h to m/s
MAX_BACKWARD_SPEED = 20 / 3.6 # 20 km/h to m/s

def scaleupdate(value):
    output_str.set(value)

def scalerelease(value):
    scale.set(SCALE_VALUE_DEFAULT)

window = ttk.Window(themename= 'journal')
window.title('Demo')
window.geometry('300x150')

# frame
frame1 = ttk.Frame(master = window)

# min_label
min_label = ttk.Label(master = frame1, text = str(SCALE_VALUE_MIN))
min_label.pack(side = 'left', padx = 10)

# scale
scalevar = tk.DoubleVar()
scale = ttk.Scale(master = frame1, variable = scalevar, from_= SCALE_VALUE_MIN, to = SCALE_VALUE_MAX, orient = 'horizontal', command = scaleupdate)
scale.set(SCALE_VALUE_DEFAULT)
scale.bind('<ButtonRelease-1>', scalerelease)
scale.pack(side = 'left', padx = 10)

# max_label
max_label = ttk.Label(master = frame1, text = str(SCALE_VALUE_MAX))
max_label.pack(side = 'left', padx = 10)

frame1.pack()

# label
output_str = tk.StringVar()
label = ttk.Label(master = window, font = 'Calibri 16', text = 'Scale value', textvariable = output_str)
label.pack(pady = 5)

# run
window.mainloop()

I tried measuring time (using timeit.timer_default()) and calculating speed by the time difference, but it would only trigger after the scale value changed.

I want an independent external timer that samples the value slider and updates the speed accordingly.

I've read about the threading.timer, but I don't understand how to integrate it with the mainloop.


Solution

  • You can use tkinter's after method to schedule a function to run after a given number of milliseconds. You can set up a repeating loop by calling after from within the function called by after.

    For example, consider this function:

    def sample(window, delay):
        # call sample(window, delay) after 'delay' milliseconds
        window.after(delay, sample, window, delay)
    
        # get the value and display it
        value = scalevar.get()
        print(f"Sample: {value:.2f}")
    

    This function will schedule itself to run again after 1000ms (1 sec). Next, it will get the current value and print it to the console. The scheduling is done first so that the amount of processing done in the function doesn't cause too much drift.

    Be aware, however, that tkinter isn't a realtime system and doesn't guarantee that a function will run precisely at the requested time. In this case, the only guarantee is that the function will be called after 1000ms. That could mean 1000.1 ms or it could be more. Since tkinter is single-threaded, the function can't be called while any other code is running.

    If you add this function to your code and then call it a single time, it will continue to run every second until you exit your program:

    # run
    sample(window, 1000)
    window.mainloop()