1

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.

Volker Siegel
  • 3,277
  • 2
  • 24
  • 35
Dania
  • 1,648
  • 4
  • 31
  • 57
  • 1
    Could you please fix indention, since indention in Python counts? – nbro Jul 06 '15 at 12:29
  • @ Christopher Wallace Yes, actually I didn't put all the code, hence the indentation is not well organized, I will edit the question. Thank you – Dania Jul 06 '15 at 15:04
  • @ Christopher Wallace I edited the question – Dania Jul 06 '15 at 15:17
  • 1
    Hi Dania! I would like to be able to help, but I am not sure I can. Anyway, not to break you, but you should provide a _working example_, which means some code that we can run and test. So you should include and the `imports`, etc. For example, I am having this error when I run your code: `NameError: name 'columnList' is not defined`. If you do it, I will open a bounty so that other people can give more attention to this post ;) – nbro Jul 06 '15 at 16:13
  • Thanks :), the first code snippet is a woking example, please check it. – Dania Jul 06 '15 at 16:39
  • You don't want the GUI to block. What is typically done is that you disable any widgets that shouldn't be clicked on. When the data is ready, you can enable the widgets. – Bryan Oakley Jul 06 '15 at 16:53
  • @BryanOakley thanks a lot, this is a great idea, can I disable the entire master window this way, self.master.config (state=DISABLED)? – Dania Jul 06 '15 at 17:00
  • No, you can't disable a parent and have all of the children widgets appear disabled. You have to disable each one individually. There are other techniques you can use, but that needs to be researched first and then asked as a separate question. – Bryan Oakley Jul 06 '15 at 17:19
  • Thanks, but the problem is that even if I disabled all widgets, still if the user clicked on the window itself while the thread is still working, the window will freeze and the OS will show that it's not responding, I don't want this to happen. Do you have any suggestions on how to solve this? Thanks. – Dania Jul 06 '15 at 17:29

1 Answers1

1

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()
Ollie
  • 473
  • 3
  • 7
  • 19
  • Thanks JSE, but I've used join to block the GUI deliberately, this is because the worker thread fill the empty variable columnList, so if I don't block the main thread until the worker thread finishes, this line, optLoc.entryconfig (0,label= columnList [0], command=self.justamethod) will be executed before the worker thread finishes, and I will get an error because still columnList is not filled. – Dania Jul 06 '15 at 16:52
  • @JSEI tried your method but I'm getting this error, AttributeError: Interface instance has no attribute 'master'. Can you please tell me how to solve this problem? – Dania Jul 06 '15 at 16:53
  • 1
    @Dania I understand why you have used `join()` but as you aren't actually doing anything in-between starting the thread and calling `join()` then there is no need for creating a separate thread, you can just do the read operation in the current thread. Have you made sure you have included the line `self.master = master` in the `__init__` method of `Interface`? The code I posted works for me in the IDLE with Python 3.4.2. – Ollie Jul 06 '15 at 17:55
  • thanks a lot, this is working nicely, but it has the same effect of using join (). My real problem is that while the interface is waiting for the read operation, if the user clicks on the window, it will freeze and the OS will indicate that it's not responding, which may show my application as not properly programmed. How can I solve this problem? for example can I block the entire window while waiting and show some loading circle..? Thanks a lot – Dania Jul 06 '15 at 20:40
  • 1
    @Dania edited my answer with another two solutions for you. Last one is probably best for your needs. – Ollie Jul 06 '15 at 23:37
  • Thanks a lot, this is really helpful. However, the waiting cursor is working well alone, but once the progressbar starts, both the waiting cursor and the bar never stop and never disappear. I've tried to use destroy, and config (value=0) with the bar, but still it won't disappear. So, I've used only the waiting indicating cursor since it works smoothly, but do you have any suggestions to solve the progressbar problem? Thanks. – Dania Jul 07 '15 at 07:49
  • 1
    @Dania Okay so I edited my code as it had a few errors in. Have you tried running just my code without any modifications? I just copied it to my work computer and it is working as expected. The `check_completed` method is what stops the Progressbar and the cursor. It checks every 50 milliseconds if the thread has finished and if it has calls `self.progressbar.stop()` to stop the progress bar, `self.progressbar.pack_forget()` to stop displaying the progress bar, `self.browse_button.config(state="enabled")` to enable the button again and `self.master.config(cursor="")` to reset the cursor. – Ollie Jul 07 '15 at 11:22
  • Thank you so much for your great help, I will try the code you posted and let you know by today. Many thanks. – Dania Jul 07 '15 at 11:42
  • thanks a lot for your help. The code needed one more line in my IDE to work well. After you stop the progressbar and the busy cursor, I needed to add, self.master.update (). If this is not added both the cursor and the bar won't change. Also, you used pack (), but in my file I use grid to place the widgets, having both in the same file usually makes some problems in the layout. So, I just changed pack () to grid(). Again your code is excellent and after these changes it's working well. You may want to update the answer and add the changes. Many thanks JSE. – Dania Jul 07 '15 at 16:55
  • 1
    Okay thanks for noting that, I'll update my answer. Yeah I just used `pack()` as it is quick and simple. I figured others can edit their layout as they see fit. No worries, I'm glad I could help. :) – Ollie Jul 07 '15 at 21:50
  • 1
    @Dania: re *"I use grid to place the widgets, having both in the same file usually makes some problems in the layout."* - just in case someone else reads that comment and gets the wrong impression, it's quite common to use them both in the same file. The only time you can't mix them is when two widgets share the same parent, but one uses `grid` and one uses `pack`. – Bryan Oakley Jul 07 '15 at 21:57
  • @BryanOakley Thank you for your useful comment :) – Dania Jul 08 '15 at 06:24