简体   繁体   中英

How to Organize Threaded GUI Application (Python)

I'm having trouble organizing my code into a useable and not super buggy program with a GUI using Tkinter (and ttk) and Python. Basically, it just downloads images off the net for now yet I'm having trouble with even a simple GUI for just that. While everything worked in the console, making the GUI was a nightmare, not to mention making it work. And well now I have it working but it crashes often and it's obvious that I'm doing things wrong getting errors of variables in the GUI not being accessed correctly (even error messages in the console that I have put along functions myself to make sure things go correctly) and constant crashes.

Basically I have something like this.

The major things that happen and need working are: User input strings from entrytext sent to the intensive part (currently contained in a thread) of the program, the intensive part stepping the progressbar of the GUI, and the intensive part sending text messages to the textbox/logger without the GUI and intensive part crashing. Also the intensive part should start as soon as the GUI is fully loaded and send start up messages to the textbox when ready.

The intensive part handles other things but does not interfere with the GUI such as the actual downloading and saving of images, browsing, and file I/O and I've had no problem, for the most part anyway.

I've also read about Queues and Threading and tutorials but I just don't seem to get it anyway. Especially how I am going to get the program to constantly step the progressbar in the GUI while also sending text messages to the GUI (How would I even approach from Queues for example without having to do very slow and CPU intensive While and If loops and multiple queues which makes it even crazier. In simple examples it's nice to just have a simple while and queue.get() wait as it consumes little resources). So my question is, what kind of structure do I need to implement for this and could I get an example or two perhaps if possible (I learn better from examples than from reading documentations)? Thank you very much.

from Tkinter import *
import ttk
import Threading
import #a bunch of other stuff

class myHardWorkerThread (threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.setDaemon(True)
        self.myClass = ModifiedConsoleClass()

    def run(self):
            #thread needs to wait at least a little otherwise the thread begins too
            #fast and causes even more errors, probably due to it sending text to
            #the textbox upon startup among other things and just overall no
            #organization
            time.sleep(3)
            self.myClass.BeginDoingStuff()

class ApplyMyGuiAndStartThread():
    def __init__(self, root, thread):

        root.geometry('500x500')
        root.resizable(0,0)

        #Put backgrounds or images or logos  here
        self.canvas = Canvas(root)
        self.canvas.pack()

        #My textbox that acts like a Log/Console output
        self.txtLogger = Text(root,state="disabled",wrap="word")
        self.txtLogger.place()
        self.scrollbar = Scrollbar(root)
        self.scrollbar.place()

        #Progressbar
        self.myVal = IntVar()
        self.TProgressbar = ttk.Progressbar(root, orient=HORIZONTAL, variable = self.myVal, mode='determinate')
        self.TProgressbar.place()

        #Entrybox for user input
        self.txbEntryText = StringVar()
        self.txtbEntry = ttk.Entry (root, textvariable=self.txbEntryText)
        self.txtbEntry.place()
        self.txtbEntry.bind("<Return>", self.SendFromGUItoThread)

        self.thread = thread
        self.thread.start()

    def SendFromGUItoThread(self,arg=None):

        myentry = str(self.txtbEntry.get())
        self.txtbEntry.delete(0, END)
        self.thread.myClass.entryBoxValueCopy = myentry


    def SendFromThreadToGUI(self,msg):
        try:
            self.txtLogger['state'] = 'normal'
            self.txtLogger.insert('end', msg)
            self.txtLogger['state'] = 'disabled'
        except:
            print "Could not be printed"


class ModifiedConsoleCode():
    def __init__(self):
        #constants here like
        self.entryBoxValueCopy = None

    def BeginDoingStuff():
        #Thread does the bulk of work  here, includes connecting to websites,
        #collecting info, sending text messages to GUI, downloading images and
        #stepping the progressbar by a calculated amount by file size

    def OneOfManyFunctionsUsedInsideBeginDoingStuff():
        #Breaks things down like looping time.sleep waits for user input in the entry box
        #showing up in entryBoxValueCopy, and using the user input to surf websites and
        #collect images

if __name__ == '__main__':

        root = Tk()
        root.title(titleOfTheProgram)

        worker = myHardWorkerThread()

        w = ApplyMyGuiAndStartThread(root,worker)

        root.mainloop()
        os._exit(0)

The short answer is, you can't interact with widgets from worker threads. Your only choice is to have your worker threads push something on a thread-safe queue, and have the main thread poll it.

You don't need any while loops to poll the queue. You already have an infinite loop -- the event loop (eg: mainloop ) -- so no need to add an extra one.

The way to poll the queue from the main thread looks something like this:

def pollQueue(self):
    <look at the queue, act on the results>
    self.after(100, self.pollQueue)

What this does is arranges to poll the queue every 100 ms. You can, of course, set the polling interval to whatever you want.

Instead of using threading you should be using the tkinter method "after" to setup an event on the tkinter event loop.

for example when using a canvas element I would use

canvar.after(50, func=keepDoingSomething)

this works similar to the javascript function setTimeout and it is thread safe and wont interfere with tkinter gui threads.

I think it would be better to create 3 classes instead of 2 and divide them into

  • GUI
  • Functions
  • App

The GUI and function are pretty self descriptive, the app is a bridge between the two, so that their work doesn't get hindered.

A sample working code is this-

import tkinter as tk
from tkinter import ttk,messagebox
import threading
import time

#base GUI Class
class GUI:
    def __init__(self, root, runCommand):
        mf = ttk.Frame(root, padding="5 5 5 5")
        mf.grid(column=0, row=0)
        mf.columnconfigure(0, weight=1)
        mf.rowconfigure(0, weight=1)

        # Global Values
        self.Fnm = tk.StringVar(root, "SearchFile.xlsx")
        self.Ncol = tk.StringVar(root, "D")
        self.Vcol = tk.StringVar(root, "C")
        # Label
        tk.Label(mf, text="File Name").grid(column=1, row=1, pady=6)
        tk.Label(mf, text="Name Col").grid(column=1, row=3, pady=6)
        tk.Label(mf, text="Value Col").grid(column=3, row=3, pady=6)

        # components
        self.fname = ttk.Entry(mf, width=18, textvariable=self.Fnm)
        self.nmCol = ttk.Entry(mf, width=6, textvariable=self.Ncol)
        self.valCol = ttk.Entry(mf, width=6, textvariable=self.Vcol)
        self.but = ttk.Button(mf, text="Refresh", command=runCommand)
        self.pgbar = ttk.Progressbar(mf, orient="horizontal", mode="determinate")

        # Design
        self.fname.grid(column=2, row=1, pady=3, columnspan=3)
        self.nmCol.grid(column=2, row=3, pady=3)
        self.valCol.grid(column=4, row=3, pady=3)
        self.but.grid(column=2, row=2, columnspan=2)
        self.pgbar.grid(column=1,row=4,columnspan=4)

    def refresh(self):
        pass

    def get(self):
        return [self.Fnm.get(), self.Ncol.get(), self.Vcol.get()]

#Base process Class
class Proc:
    def __init__(self, dets,pgbar,but):
        self.Fnm = dets[0]
        self.Ncol = dets[1]
        self.Vcol = dets[2]
        self.pg=pgbar
        self.butt=but

    def refresh(self):
        self.butt['state'] = 'disabled'
        self.pg.start()
        #ATTENTION:Enter Your process Code HERE
        for _ in range(5):
            time.sleep(2)
        self.pg.stop()
        #Any search/sort algorithm to be used
        #You can use self.pg.step() to be more specific for how the progress bar proceeds
        messagebox.showinfo("Process Done","Success")
        self.butt['state'] = 'enabled'

#Base Application Class
class App:
    def __init__(self, master):
        self.master = master
        self.gui = GUI(self.master, self.runit)

    def runit(self):
        self.search = Proc(self.gui.get(),self.gui.pgbar,self.gui.but)
        self.thread1 = threading.Thread(target=self.search.refresh)
        self.thread1.start()

def main():
    app = tk.Tk()
    gui = App(app)
    app.title("Refresh Search File")
    app.mainloop()

if __name__ == '__main__':
    main()

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