简体   繁体   中英

How to correctly implement multithreading using a Tkinter GUI?

I am trying to implement Multithreading while using a GUI in tkinter. I came along this solution , but i am failing to implement it.

So basically i need to know:

How do i need to alter my Code to make the Progressbar interact nice and smoothly, ie not going in unresponsive mode when the GUI loses focus?

This is my code boiled down:

from tkinter import *
import queue
import threading
from tkinter.ttk import *

class ThreadedTask(threading.Thread):
    def __init__(self, queue):
        threading.Thread.__init__(self)
        self.queue = queue  

# Gui class
class MyGui(Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent
        self.queue = queue.Queue() 
        self.init_ui()

    # define a button to fulfill long task
    def init_ui(self):        
        self.frame = Frame(self, relief=RAISED, borderwidth=1)
        self.frame.grid(row=0, column=0, columnspan=1, sticky='ns')
        self.status_frame = Frame(self, relief=RAISED, borderwidth=1, height=20)
        self.status_frame.grid(row=1, column=0, columnspan=3, sticky='nesw')
        self.status_frame.grid_configure(padx=3, pady=3)
        self.button = Button(self.frame, text='do Stuff', command=self.do_stuff)
        self.button.grid(padx=3, pady=3)
        self.progress = Progressbar(self.status_frame, orient="horizontal", length=80, mode="determinate")
        self.progress.grid(row=1, column=0, pady=3, sticky='nesw')
        self.grid()

    # start ThreadedTask here 
    def do_stuff(self):
        ThreadedTask(self.queue).start()
        self.queue_process(DoStuffClass(ui=self))

    def queue_process(self, process, retry_time=10): 
        self.master.after(retry_time, process) 

    def update_status(self):
        self.parent.update_idletasks()

class DoStuffClass:
    def __init__(self, ui=None):
        self.ui = ui
        self.long_task()

    def long_task(self):
        # do stuff here and update the progressbar from MyGui
        self.ui.progress['maximum'] = 10000
        # some lengthy task ...
        for i in range(10000):
            print(i)
            self.ui.progress.step()
            self.ui.parent.update_idletasks()


# main
root = Tk()
root.geometry("150x80+50+50")
MyGui(root)
root.mainloop()
root.quit()

I think right now my problem is the wrong implementation of queues and Threading, ie self.queue_process(DoStuffClass(ui=self)) ... the behaviour is the same as if i wouldn't use a queue and Multithread at all. The progressbar works, as long as it stays "in focus", which means me not clicking anything else on the desktop. As soon as i'm clicking elsewhere on the desktop and the GUI loses focus, the GUI goes in "unresponsive" mode and the progress bar does not update anymore. Also, sometimes Tcl closes the wrong Thread, which makes the whole program crash.

So after a couple of tries i figured out what to do:

from tkinter import *
import queue
import threading
from tkinter.ttk import *

class ThreadedTask(object):
    def __init__(self, parent):
        self.parent = parent        
        self.queue = queue.Queue()
        self.gui = MyGui(parent, self.queue)
        self.work_queue()

    def work_queue(self):
        """ Check every 100 ms if there is something new in the queue. """
        try:
            self.parent.after(200, self.work_queue)
            print('working queue with task {}...'.format(self.queue.get_nowait()))
        except queue.Empty:
            pass


# Gui class
class MyGui(Frame):
    def __init__(self, parent, queue):
        super().__init__(parent)
        self.parent = parent
        self.queue = queue
        self.init_ui()

    # define a button to fulfill long task
    def init_ui(self):   
        self.frame = Frame(self, relief=RAISED, borderwidth=1)
        self.frame.grid(row=0, column=0, columnspan=1, sticky='ns')
        self.status_frame = Frame(self, relief=RAISED, borderwidth=1, height=20)
        self.status_frame.grid(row=1, column=0, columnspan=3, sticky='nesw')
        self.status_frame.grid_configure(padx=3, pady=3)        
        self.button = Button(self.frame, text='do Stuff', command=self.init_button_loop)
        self.button.grid(padx=3, pady=3)
        self.progress = Progressbar(self.status_frame, orient="horizontal", length=80, mode="determinate")
        self.progress.grid(row=1, column=0, pady=3, sticky='nesw')
        self.grid()


    def start_thread(self, function_name, queue):
        t = threading.Thread(target=function_name, args=(queue,))
        # close thread automatically after finishing task
        t.setDaemon(True)
        t.start()

    # execute button push by spawning a new thread
    def init_button_loop(self):
        self.start_thread(self.exec_button_loop, self.queue)    

    # execute long task
    def exec_button_loop(self, queue):
        self.progress['maximum'] = 10000
        for i in range(10000):
            # update progressbar
            queue.put(self.progress.step())


# main
root = Tk()
root.geometry("150x80+50+50")
client = ThreadedTask(root)
root.mainloop()

The Difficulty was to figure out how to interact with queues and threads, while pressing buttons in the gui.

Basically my fault was to declare the queue in the wrong class and the wrong use of the after function while not knowing where to start the threads.

The new implementation follows the principle of filling the queue up in a new thread, which is spawned when pressing the gui button. The queue is periodically checked from the main thread whether theres something to do. This prevents unresponsiveness due to a safe communication between gui and mainthread.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM