Search code examples
pythonmultithreadinguser-interfacetkinterprogress-bar

Blocking Tkinter interface until thread finishes its task


I am writing a program in python that will process a big amount of data it reads from some excel file. I've build a GUI for this program using Tkinter. I know Tkinter is single threaded, hence to open the file and make some processes I've used a thread not to block the GUI. One of the thread tasks is to fill a list (called columnList in my code) and use its elements as options in an optionmenu, so before the thread finishes the option menu is empty, hence I've used join () to let the main thread wait for the worker thread. And, here comes the problem, as long as the worker thread is executing the GUI will be not responding (around 7 seconds), but after that it will work normally.

I want to use some graphical indicator that indicates that something is being loaded, and at the same time blocks the the GUI window so the user can't click on it. After the thread stops, the indicator should disappear and the GUI should be enabled again. I searched for such a concept, but I didn't find such a thing on the web, this question here, Python Tkinter: loading screen is very similar to my case, but it has no answer.

This is a part of my code where I need to apply the concept:
(working code example)

__author__ = 'Dania'
import threading


from Tkinter import *
from tkFileDialog import askopenfilename
import numpy as np
import xlrd
global x
global v

x = np.ones(5)
v= np.ones(5)
global columnList
columnList=""


def open_file (file_name):
    

    try:
        workbook = xlrd.open_workbook(file_name)
        sheet=workbook.sheet_by_index(0)
        global columns
        columns = [] #this is a list, in each index we will store a numpy array of a column values.
        for i in range (0,sheet.ncols-1):
           columns.append(np.array (sheet.col_values(i,1))) # make a list, each index has a numpy array that represnts a column. 1 means start from row 1 (leave the label)
           if (i!=0):
               columns[i]= columns[i].astype(np.float)
        #Preprocessing columns[0]:
        m= columns [0]
        for i in range (m.shape[0]):
            m[i]= m[i]*2 +1

        m=m.astype(np.int)
        columns[0]=m

        global columnList
        columnList= np.array(sheet.row_values(0)) #I was using sheet.row(0), but this is better since it doesn't return a 'u'
        columnList=columnList.astype(np.str)
        
        
        # removing nans:
        index=input("enter the column index to interpolate: ") #this should be user input

        n= columns [index]
        for i in range (n.shape[0]-1, -1, -1):
            if (np.isnan(n[i])):
                n=np.delete(n,i)
                columns[0]=np.delete(columns[0],i)
                columns [index]= np.delete(columns[index],i)


    except IOError:
        print ("The specified file was not found")

    global x
    np.resize(x, m.shape[0])
    x=columns[0]

    global v
    np.resize(v,n.shape[0])
    v=columns[index]
    
    #return columns [0], columns [index]


    

class Interface:
    def __init__(self, master):

        self.title= Label(master,text="Kriging Missing data Imputation", fg="blue", font=("Helvetica", 18))
        self.select_file= Label (master, text="Select the file that contains the data (must be an excel file): ", font=("Helvetica", 12))


        self.title.grid (row=1, column=5, columnspan= 4, pady= (20,0))
        self.select_file.grid (row=3, column=1, sticky=W, pady=(20,0), padx=(5,2))
        self.browse_button= Button (master, text="Browse", command=self.browser, font=("Helvetica", 12), width=12)
        self.browse_button.grid (row=3, column=3, pady=(20,0))


        self.varLoc= StringVar(master)
        self.varLoc.set("status")

        self.varColumn= StringVar(master)
        self.varColumn.set("")

        self.locationColumn= Label(master,text="Select a column as a location indicator", font=("Helvetica", 12))
        self.columnLabel= Label(master,text="Select a column to process", font=("Helvetica", 12))

        global locationOption
        global columnOption
        columnOption= OptionMenu (master, self.varColumn,"",*columnList)
        locationOption= OptionMenu (master, self.varLoc,"",*columnList)

        self.locationColumn.grid (row=5, column=1, pady=(20,0), sticky=W, padx=(5,0))
        locationOption.grid (row=5, column=3, pady=(20,0))

        self.columnLabel.grid (row=7, column=1, pady=(20,0), sticky=W, padx=(5,0))
        columnOption.grid(row=7, column= 3, pady= (20,0))


        self.missing_label= Label(master, text="Select missing data indicator: ", font=("Helvetica", 12))
        self.var = StringVar (master)
        self.var.set("nan")
        self.menu= OptionMenu (master, self.var,"nan", "?", "*")

        self.missing_label.grid (row=9, column=1, padx=(5,2), pady= (20,0), sticky=W)
        self.menu.grid(row=9, column=3, pady= (20,0))

        self.extrapolate= Label (master, text="Select a range for extrapolation (max=800): ", font=("Helvetica", 12))
        self.max_extra= Entry (master)

        self.extrapolate.grid (row=11, column=1, padx=(5,2), pady= (20,0),  sticky=W)
        self.max_extra.grid (row=11, column=3, pady=(20,0))

        self.a_label= Label (master, text="enter the value of a (range): ", font=("Helvetica", 12))
        self.a_value= Entry (master)

        self.a_label.grid (row=13, column=1, padx=(5,2), pady=(20,0),  sticky=W)
        self.a_value.grid (row=13, column=3,  pady=(20,0))


        self.start_button= Button (master, text="Start", font=("Helvetica", 12), width=12)
        self.pause_button= Button (master, text= "Pause", font=("Helvetica", 12),width=12)
        self.stop_button= Button (master, text="stop", font=("Helvetica", 12),width=12)

        self.start_button.grid (row=15, column=1, pady=(30,0) )
        self.pause_button.grid (row=15, column=2, pady=(30,0))
        self.stop_button.grid (row=15, column=3, pady=(30,0))



    def browser (self):
            filename = askopenfilename()
            #indicator should start here.
            t=threading.Thread (target=open_file, args=(filename, ))
           
            t.start()
            t.join() #I use join because if I didn't,next lines will execute before  open_file is completed, this will make columnList empty and the code will not execute.
            #indicator should end here. 
            opt=columnOption.children ['menu']
            optLoc= locationOption.children ['menu']
            optLoc.entryconfig (0,label= columnList [0], command=self.justamethod)
            opt.entryconfig (0, label= columnList [0], command=self.justamethod)
            for i in range(1,len (columnList)):
                opt.add_command (label=columnList[i], command=self.justamethod)
                optLoc.add_command (label=columnList[i], command=self.justamethod)

    def justamethod (self):
        print("method is called")
        print(self.varLoc.get())




window= Tk () #main window.
starter= Interface (window)


window.mainloop() #keep the window open until the user decides to close it.

I've tried to add some progress bar inside the method browser like this,

 def browser (self):
            filename = askopenfilename()
            progressbar = ttk.Progressbar(orient=HORIZONTAL, length=200, mode='determinate')
            progressbar.pack(side="bottom")
            progressbar.start()
            t=threading.Thread (target=open_file, args=(filename, ))
            t.start()
            t.join() #I use join because if I didn't,next lines will execute before  open_file is completed, this will make columnList empty and the code will not execute.
            progressbar.stop()
            opt=columnOption.children ['menu']
            opt.entryconfig (0, label= columnList [0], command=self.justamethod)

            for i in range(1,len (columnList)):
                opt.add_command (label=columnList[i], command=self.justamethod)
                optLoc.add_command (label=columnList[i], command=self.justamethod)

 def justamethod (self):
        print("method is called")
        
           

window= Tk () #main window.
starter= Interface (window)


window.mainloop() #keep the window open until the user decides to close it.

But, the code above doesn't even show the progress bar, and it's not what I really need.


Solution

  • One of the advantages of using a background thread to read the file is so the current thread doesn't block and can continue to function. By calling t.join() straight after t.start you are blocking the GUI no differently to if you just did the read in the current thread.

    Instead how about you just change the cursor to a wait cursor before you do the operation? I have simplified your code but something like this:

    from tkinter import *
    import time
    
    class Interface:
        def __init__(self, master):
            self.master = master
            self.browse_button= Button (master, text="Browse", command=self.browser)
            self.browse_button.pack()
    
        def browser (self):
            self.master.config(cursor="wait")
            self.master.update()
            self.read_file("filename")
            self.master.config(cursor="")
    
        def read_file (self, filename):
            time.sleep(5)  # actually do the read file operation here
    
    window = Tk()
    starter = Interface(window)
    window.mainloop()
    

    EDIT: Okay I think I understand better what the issue is. My OS doesn't say not responding so can't really test the issue but try this with a Thread and a Progressbar.

    from tkinter import *
    from tkinter.ttk import *
    import time
    import threading
    
    class Interface:
        def __init__(self, master):
            self.master = master
            self.browse_button= Button (master, text="Browse", command=self.browser)
            self.browse_button.pack()
            self.progressbar = Progressbar(mode="determinate", maximum=75)
    
        def browser (self):
            t = threading.Thread(target=self.read_file, args=("filename",))
            self.progressbar.pack()
            self.browse_button.config(state="disabled")
            self.master.config(cursor="wait")
            self.master.update()
    
            t.start()
            while t.is_alive():
                self.progressbar.step(1)
                self.master.update_idletasks()  # or try self.master.update()
                t.join(0.1)
    
            self.progressbar.config(value="0")
            self.progressbar.pack_forget()
            self.browse_button.config(state="enabled")
            self.master.config(cursor="")
    
        def read_file (self, filename):
            time.sleep(7)  # actually do the read here
    
    window = Tk()
    starter = Interface(window)
    window.mainloop()
    

    NOTE: I've not done much GUI coding and this may not be the best solution just passing through and trying to help! :)

    EDIT 2: Thought about it a bit more. As you are unsure how long exactly the read will take, you could use this method that just bounces the indicator back and forth between the ends of the progress bar.

    from tkinter import *
    from tkinter.ttk import *
    import time
    import threading
    
    class Interface:
        def __init__(self, master):
            self.master = master
            self.browse_button= Button (master, text="Browse", command=self.browser)
            self.browse_button.pack()
            # Create an indeterminate progressbar here but don't pack it.
            # Change the maximum to change speed. Smaller == faster.
            self.progressbar = Progressbar(mode="indeterminate", maximum=20)
    
        def browser (self):
            # set up thread to do work in
            self.thread = threading.Thread(target=self.read_file, args=("filename",))
            # disable the button
            self.browse_button.config(state="disabled")
            # show the progress bar
            self.progressbar.pack()
            # change the cursor
            self.master.config(cursor="wait")
            # force Tk to update
            self.master.update()
    
            # start the thread and progress bar
            self.thread.start()
            self.progressbar.start()
            # check in 50 milliseconds if the thread has finished
            self.master.after(50, self.check_completed)
    
        def check_completed(self):
            if self.thread.is_alive():
                # if the thread is still alive check again in 50 milliseconds
                self.master.after(50, self.check_completed)
            else:
                # if thread has finished stop and reset everything
                self.progressbar.stop()
                self.progressbar.pack_forget()
                self.browse_button.config(state="enabled")
                self.master.config(cursor="")
                self.master.update()
    
                # Call method to do rest of work, like displaying the info.
                self.display_file()
    
        def read_file (self, filename):
            time.sleep(7)  # actually do the read here
    
        def display_file(self):
            pass  # actually display the info here
    
    window = Tk()
    starter = Interface(window)
    window.mainloop()