Search code examples
pythontkinterschedule

Start and stop a repeating task in a thread from a button in tkinter


I want to start a task, which should run every 10 seconds, from a GUI made in tkinter. The GUI has a start button and a stop button, with an event to stop the thread. I am using the schedule module and threading to avoid freezing the GUI.

from functools import partial
import tkinter as tk
from threading import Thread, Event, Lock
import schedule
import time
import os


class GUI():
    def __init__(self,master):
        self.master = master
        self.frame = tk.Frame(self.master)
        
        self.button1 = tk.Button(text='Start', command=partial(run_threaded,experiment), width=20,height=2,bg="gray",fg="black")
        self.button1.pack()
        self.button2 = tk.Button(text='Stop', command=stop_button, width=20,height=2,bg="gray",fg="black")
        self.button2.pack()
    
def job():
    print("Task started") 
    print("Task completed") 
        
def experiment():
    schedule.every(10).seconds.do(job)
    while 1:
        schedule.run_pending()
    
def run_threaded(job_func):
    job_thread = Thread(target=job_func)
    job_thread.start()
    if stop_threads.is_set():
        job_thread.join()
        job_thread = None

def start_button():
    run_threaded(experiment)
    
def stop_button():
    global stop_threads
    stop_threads.set()

def main():
    global stop_threads
    thread1=None
    stop_threads = Event()
    root = tk.Tk()
    app = GUI(root)
    root.mainloop()

if __name__ == '__main__':
    main()

The task starts after 10 seconds and repeats periodically, but the stop button does not work and I would like to have the process start immediately, without the initial delay.

Furthermore, every other time I run it, the task starts but the GUI does not appear.

Is there a better way to accomplish this?


Solution

  • Try something like this:

    import tkinter as tk
    from time import sleep
    from functools import partial
    from threading import Thread
    
    running_job = False
    
    
    def job():
        print("Task started") 
        print("Task completed") 
    
    def experiment():
        while running_job:
            job()
            # I changed it to 1 sec to make it easier to test
            sleep(1)
    
    
    class GUI:
        def __init__(self,master):
            self.master = master
            self.frame = tk.Frame(self.master)
    
            command = partial(run_threaded, experiment)
            self.button1 = tk.Button(text="Start", command=command)
            self.button1.pack()
            self.button2 = tk.Button(text="Stop", command=stop_button)
            self.button2.pack()
    
    
    def run_threaded(job_func):
        global running_job
        running_job = True
        job_thread = Thread(target=job_func, daemon=True)
        job_thread.start()
    
    def stop_button():
        global running_job
        running_job = False
    
    if __name__ == "__main__":
        root = tk.Tk()
        app = GUI(root)
        root.mainloop()
    

    It uses a global variable named running_job. When running_job is True the while loop inside experiment can run. When it turns to False, experiment stops. The problem with this approach is that if you use tkinter widgets/variables inside your job function, tkinter may crash.

    Another approach would be using .after scripts instead but for that to work the job function shouldn't take long to execute.