Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best way to stop a thread and avoid 'RuntimeError' in python using threading and tkinter modules?

I have went through multiple solutions on the net, but they require a lot of code that might get confusing once you scale up. Is there a simple way to stop the thread and avoid the RuntimeError: threads can only be started once, in order to call the thread an infinite number of times. Here is a simple version of my code:

import tkinter
import time
import threading

def func():

    entry.config(state='disabled')
    label.configure(text="Standby for  seconds")
    time.sleep(3)
    sum = 0
    for i in range(int(entry.get())):
        time.sleep(0.5)
        sum = sum+i
        label.configure(text=str(sum))
    entry.config(state='normal')

mainwindow = tkinter.Tk()
mainwindow.title('Sum up to any number')

entry = tkinter.Entry(mainwindow)
entry.pack()
label = tkinter.Label(mainwindow, text = "Enter an integer",font=("Arial",33))
label.pack()

print(entry.get())

button = tkinter.Button(mainwindow, text="Press me", command=threading.Thread(target=func).start)
button.pack()
like image 717
Samir Ahmane Avatar asked Nov 07 '22 07:11

Samir Ahmane


1 Answers

It is possible to call modifications on tkinter widgets from other threads, and they will occur as soon as the main thread is available, which may be immediately. If the background thread calling the modifications sleeps while the main thread is only in mainloop, we can simulate a pause in the app without blocking on the main thread as the question aims for.

Then we can subclass Thread to produce a thread that runs its own loop and remains started even after its target finishes so that we can call its target as many times as we like. We can even pass errors that occur on the background thread through and gracefully exit the thread without hanging the app by using daemon mode and a try-except block.

The BooleanVar thread.do acts as a switch that we can set in a lambda to run func once on the thread when button is pressed. This implements a cheap messaging system between the main and background threads which we could extend with little extra code to allow calling func with arguments and returning values from it.

import threading, time, tkinter, sys

class ImmortalThread(threading.Thread):
  def __init__(self, func):
    super().__init__(daemon=True)
    self.func = func
    self.do = tkinter.BooleanVar()
  def run(self):
    while True:
      if self.do.get():
        try:
          self.func()
          self.do.set(False)
        except:
          print("Exception on", self, ":", sys.exc_info())
          raise SystemExit()
      else:
        # keeps the thread running no-ops so it doesn't strip
        time.sleep(0.01)

def func():
  entry.config(state='disabled')
  label.configure(text="Standby for  seconds")
  time.sleep(3)
  sum = 0
  for i in range(int(entry.get())):
    time.sleep(0.5)
    sum = sum+i
    label.configure(text=str(sum))
  entry.config(state='normal')

mainwindow = tkinter.Tk()
mainwindow.title("Sum up to any number")

entry = tkinter.Entry(mainwindow)
entry.pack()
label = tkinter.Label(mainwindow, text="Enter an integer", font=("Arial", 33))
label.pack()

thread = ImmortalThread(func)
thread.start()
button = tkinter.Button(mainwindow, text="Press me", command=lambda: thread.do.set(True))
button.pack()

mainwindow.mainloop()
like image 109
Evan Messinger Avatar answered Nov 14 '22 22:11

Evan Messinger