I'm pretty new to Python in general and very new to Tk/ttk. But here's an example of what I've been playing with for event triggering/signaling and worker thread stuff in Tk/ttk. I know some people will hate the singleton decorator and I know there are other ways to call code from other classes but the trigger class is very convenient and the worker class works like a charm. Together they make things super easy.
Credits:
The worker class is a very slightly modified version of the GObject worker found in Pithos and the singleton decorator is a very slightly modified version of something I found here on stackoverflow somewhere.
import sys
import tkinter
from tkinter import ttk
from tkinter import StringVar
import threading
import queue
import traceback
import time
class TkWorkerThreadDemo:
def __init__(self):
self.root = tkinter.Tk()
self.trigger = Trigger.Singleton()
self.trigger.connect_event('enter_main_thread', self.enter_main_thread)
self.worker = Worker()
self.root.title('Worker Thread Demo')
self.root.resizable(width='False', height='False')
self.test_label_text = StringVar()
self.test_label_text.set('')
self.slider_label_text = StringVar()
self.slider_label_text.set('Press either button and try to move the slider around...')
mainframe = ttk.Frame(self.root)
test_label = ttk.Label(mainframe, anchor='center', justify='center', textvariable=self.test_label_text)
test_label.pack(padx=8, pady=8, fill='x')
slider_label = ttk.Label(mainframe, anchor='center', justify='center', textvariable=self.slider_label_text)
slider_label.pack(padx=8, pady=8, expand=True, fill='x')
self.vol_slider = ttk.Scale(mainframe, from_=0, to=100, orient='horizontal', value='100', command=self.change_slider_text)
self.vol_slider.pack(padx=8, pady=8, expand=True, fill='x')
test_button = ttk.Button(mainframe, text='Start Test with a Worker Thread', command=self.with_worker_thread)
test_button.pack(padx=8, pady=8)
test_button = ttk.Button(mainframe, text='Start Test in the Main Thread', command=self.without_worker_thread)
test_button.pack(padx=8, pady=8)
mainframe.pack(padx=8, pady=8, expand=True, fill='both')
self.root.geometry('{}x{}'.format(512, 256))
def enter_main_thread(self, callback, result):
self.root.after_idle(callback, result)
def in_a_worker_thread(self):
msg = 'Hello from the worker thread!!!'
time.sleep(10)
return msg
def in_a_worker_thread_2(self, msg):
self.test_label_text.set(msg)
def with_worker_thread(self):
self.test_label_text.set('Waiting on a message from the worker thread...')
self.worker.send(self.in_a_worker_thread, (), self.in_a_worker_thread_2)
def in_the_main_thread(self):
msg = 'Hello from the main thread!!!'
time.sleep(10)
self.in_the_main_thread_2(msg)
def in_the_main_thread_2(self, msg):
self.test_label_text.set(msg)
def without_worker_thread(self):
self.test_label_text.set('Waiting on a message from the main thread...')
self.root.update_idletasks()#without this the text wil not get set?
self.in_the_main_thread()
def change_slider_text(self, slider_value):
self.slider_label_text.set('Slider value: %s' %round(float(slider_value)))
class Worker:
def __init__(self):
self.trigger = Trigger.Singleton()
self.thread = threading.Thread(target=self._run)
self.thread.daemon = True
self.queue = queue.Queue()
self.thread.start()
def _run(self):
while True:
command, args, callback, errorback = self.queue.get()
try:
result = command(*args)
if callback:
self.trigger.event('enter_main_thread', callback, result)
except Exception as e:
e.traceback = traceback.format_exc()
if errorback:
self.trigger.event('enter_main_thread', errorback, e)
def send(self, command, args=(), callback=None, errorback=None):
if errorback is None: errorback = self._default_errorback
self.queue.put((command, args, callback, errorback))
def _default_errorback(self, error):
print("Unhandled exception in worker thread:\n{}".format(error.traceback))
class singleton:
def __init__(self, decorated):
self._decorated = decorated
self._instance = None
def Singleton(self):
if self._instance:
return self._instance
else:
self._instance = self._decorated()
return self._instance
def __call__(self):
raise TypeError('Singletons must be accessed through `Singleton()`.')
@singleton
class Trigger:
def __init__(self):
self._events = {}
def connect_event(self, event_name, func, *args, **kwargs):
self._events[event_name] = func
def disconnect_event(self, event_name, *args, **kwargs):
if event_name in self._events:
del self._events[event_name]
def event(self, event_name, *args, **kwargs):
if event_name in self._events:
return self._events[event_name](*args, **kwargs)
def main():
demo = TkWorkerThreadDemo()
demo.root.mainloop()
sys.exit(0)
if __name__ == '__main__':
main()