0

I am posting this as informational for folks looking for a way to communicate thread progress back to a tkinter Frame or window. I have seen several approaches detailed in SO and other sites, but none really seemed adequate to me. So here is an approach that displays progress as both a message box update and advancing a Scale Widget. It uses the tkinter variable classes StringVar and DoubleVar rather than trying to use callbacks or continuously poll a queue in the main thread.

Comments are, of course, welcome, but it appears this approach works well.

`

import tkinter as tk
from tkinter import ttk, END, NW, GROOVE
import threading
import queue
import time


class App(tk.Tk):

    def __init__(self):
      tk.Tk.__init__(self)
      self.queue = queue.Queue()
      self.msgCt=0
      self.listbox = tk.Listbox(self, width=20, height=5)
      self.scaleVal=tk.DoubleVar()
      self.progressbar = ttk.Scale(self, orient='horizontal',
                                         length=300,
                                         from_=0.0, to=100.0,
                                         variable=self.scaleVal)
      self.scaleVal.set(0.0)
      self.button = tk.Button(self, text="Start", command=self.spawnthread)
      self.msgBtn = tk.Button(self,text="Set Msg", command=self.sendMessage)
      self.msgTxt=tk.StringVar(self,"Messages Here...")
      self.msgBox=tk.Message(self,textvariable=self.msgTxt,width=200,
                             anchor=NW,relief=GROOVE)

      self.listbox.grid(row=0,column=0,columnspan=2)
      self.msgBox.grid(row=1,column=0,columnspan=2)
      self.progressbar.grid(row=2,column=0,columnspan=2)
      self.button.grid(row=3,column=0)
      self.msgBtn.grid(row=3,column=1)

    def spawnthread(self):
      self.button.config(state="disabled")
      self.listbox.delete(0, END)
      self.thread = ThreadedClient(self.queue,self.msgTxt,self.scaleVal)
      self.thread.start()
      self.periodiccall()

    def sendMessage(self,msg=None):
      if not msg==None:
        self.msgTxt.set(msg)
      else:
        self.msgTxt.set("Message {}".format(self.msgCt))
      self.msgCt+=1

    def periodiccall(self):
        self.checkqueue()
        if self.thread.is_alive():
            self.after(100, self.periodiccall)
        else:
            self.button.config(state="active")

    def checkqueue(self):
        while self.queue.qsize():
            try:
                msg = self.queue.get(0)
                self.listbox.insert('end', msg)
                # self.progressbar.step(25)
            except queue.Empty:
                pass


class ThreadedClient(threading.Thread):

    def __init__(self, qu, mtxt,dvar):
        threading.Thread.__init__(self)
        self.queue = qu
        self.msgTxt=mtxt
        self.scaleVal=dvar

    def run(self):
      self.scaleVal.set(0.0)
      for x in range(1, 10):
          time.sleep(2)
          msg = "Function %s finished..." % x
          self.msgTxt.set(msg)
          self.scaleVal.set(x*10)
          self.queue.put(msg)


if __name__ == "__main__":
    app = App()
    app.mainloop()

`

CaseyB66
  • 25
  • 6
  • ***"rather than trying to use callbacks or continuously poll a queue"***: This is contradict to the usage of: `.after(100, self.periodiccall)` and `self.queue = queue.Queue()`. Why don't you use a `Progressbar` widget? Setting a `tk.DoubleVar` which is bound to GUI widget violates that all Tcl commands need to originate from the same thread. [Threads/Process and Tkinter](https://stackoverflow.com/questions/26703502) – stovfl Mar 20 '20 at 19:12
  • Sorry, the .after(100, self.periodiccall) is left in to demonstrate how others have accomplished the task, that is where the queue gets polled. This whole thing would work without the queue however, just some long running process that does something else. You say ''all Tcl commands need to originate from the same thread". That is why I use tk.DoubleVar instead -- it is just a variable, not a command or function call, but when changed (apparently from another thread, based on this code) triggers a widget update in the main thread. – CaseyB66 Mar 20 '20 at 20:44
  • I dont use progressBar because I did not see that it can take a optional variable (e.g. IntVar or DoubleVar). Did I miss something there (I do find the official python doc to be pretty sparse sometimes...) – CaseyB66 Mar 20 '20 at 20:45
  • ***"but when changed (apparently from another thread) triggers a widget update in the main thread."***: Do you have a reference about this or do you have proven this? – stovfl Mar 20 '20 at 20:57
  • ***"can take a optional variable"***: According to [ttk_progressbar](http://www.tcl.tk/man/tcl8.6/TkCmd/ttk_progressbar.htm), it can. – stovfl Mar 20 '20 at 21:00
  • My only reference for my assumption that changing DoubleVar in the running thread triggers an update in the main thread is the program I posted (see code above...). It works! Also, I did not mention, i put a push button on the main window that will send a different text to the message box, 'proving' that changing the StringVar will trigger a Widget update from either the running thread or main thread. In real code, if a person really wanted to do that, I suppose you would have to make access to that variable thread safe. – CaseyB66 Mar 20 '20 at 22:46
  • `StringVar` or `DoubleVar` is not just variable, it is a class. How do you know that the update is in the main thread? – acw1668 Mar 21 '20 at 00:34

1 Answers1

0

In response to the several comments, here is a new version of the code:

import tkinter as tk
from tkinter import ttk, END, NW, GROOVE
import threading
import queue
import time
from pickle import FALSE


class App(tk.Tk):

    def __init__(self):
      tk.Tk.__init__(self)
      self.msgCt=0
      self.thrdCt=0;
      self.scaleVal=tk.DoubleVar()
      self.doneSignal=tk.BooleanVar()
      self.doneSignal.set(False)
      self.doneSignal.trace("w",self.on_doneSignal_set)
      self.progressbar = ttk.Progressbar(self, orient='horizontal',
                                         length=300,
                                         maximum=100.0,
                                         variable=self.scaleVal)
      self.scaleVal.set(0.0)
      self.startBtn = tk.Button(self, text="Start", command=self.spawnthread)
      self.stopBtn=tk.Button(self,text="Stop", command=self.stopthread)
      self.stopBtn.config(state="disabled")
      self.msgBtn = tk.Button(self,text="Set Msg", command=self.sendMessage)
      self.msgTxt=tk.StringVar(self,"Messages Here...")
      self.msgBox=tk.Message(self,textvariable=self.msgTxt,width=200,
                             anchor=NW,relief=GROOVE)

      self.msgBox.grid(row=0,column=0,columnspan=3)
      self.progressbar.grid(row=1,column=0,columnspan=3)
      self.startBtn.grid(row=2,column=0)
      self.stopBtn.grid(row=2,column=1)
      self.msgBtn.grid(row=2,column=2)

    def on_doneSignal_set(self,*kwargs):
      self.sendMessage("Thread is DONE")
      self.startBtn.config(state="active")
      self.stopBtn.config(state="disabled")

    def stopthread(self):
      if self.thread.is_alive():
        self.thread.stopNow()

    def spawnthread(self):
      self.thrdCt=0
      self.startBtn.config(state="disabled")
      self.stopBtn.config(state="active")
      self.thread = ThreadedClient(self.msgTxt,self.scaleVal,self.doneSignal)
      self.thread.start()
      # self.periodiccall()

    def sendMessage(self,msg=None):
      if not msg==None:
        self.msgTxt.set(msg)
      else:
        self.msgTxt.set("Message {}".format(self.msgCt))
      self.msgCt+=1

class ThreadedClient(threading.Thread):

    def __init__(self, mtxt,dvar,dsig):
      threading.Thread.__init__(self)
      self.msgTxt=mtxt
      self.scaleVal=dvar
      self._stopNow=False
      self._doneSignal=dsig
      self._lock=threading.Lock()

    def run(self):
      self._stopNow=False
      self.scaleVal.set(0.0)
      for x in range(1, 10):
        if not self.checkStopNow():
          time.sleep(2)
          msg = "Function %s finished..." % x
          self.msgTxt.set(msg)
          self.scaleVal.set(x*10)
        else:
          break
      self._doneSignal.set(True)

    def stopNow(self):
      with self._lock:
        self._stopNow=True

    def checkStopNow(self):
      rtrn=False
      with self._lock:
        rtrn=self._stopNow
      return rtrn

if __name__ == "__main__":
    app = App()
    app.mainloop()

First, the motivation for this exercise in the first place: I have a large python application that uses Scipy.optimize to find a solution to a modeling problem. This can often take a long time, so I wanted a way to have it run, but post messages to the user periodically to let them know things are happening, allow user to abort in the middle, and finally to post a message to the main thread that the modeling is now done. My original code was partly based on a threading example that assumed the producer/consumer model of threading, whereby a producer creates data (put into a Queue) and the consumer consumes it. This is the WRONG model for my problem, so this new code has no Queue. It just simulates a long modeling process using the run() method, in which there are calls to sleep() which could obviously be replaced by steps in the SciPy.minimize function calls.

This new example uses DoubleVar to allow the thread to update a progressBar (thanks to stovfl for that suggestion), a StringVar to update a message box in the main thread from the modeling thread, and finally a BooleanVar to signal the main thread that things are done. This version has no polling in the main thread. To me, that does not seem a very elegant solution!

How do I know the changes to the DoubleVar, StringVar and BooleanVar get through to the main thread? Only that this program works!! Note that the message box can be updated from either the modeling thread or using a button in the main GUI thread.

Again, comments welcome -- Give me reasons this should NOT work, then tell me why it does given those reasons! Does this violate some basic design of Python, or would there be a situation where this would not work for some reason??

CaseyB66
  • 25
  • 6
  • Are you sure this solves your problem completely? Is this the correct answer? Can you mark it as solved if this is the solution? – Dharman Mar 22 '20 at 16:27
  • Just checked indicating solution, this latest version seems to solve my problem. But the question I posed is does anyone else see a fundamental reason why this should not work? Still waiting for some killer comment from someone that knows python better than I – CaseyB66 Mar 22 '20 at 17:40