Search code examples
pythonpython-3.xtkinterttk

tkinter progress bar won't update when called from an other app


I am developing a GUI with tkinter to manage images in a database (import file, load file, query, ...)

When a new directory and its sub-directories are scanned for new images to put in the database, a dedicated GUI is launched: It consists of a Text widget where the name of the directory currently analysed is printed, and a progress-bar showing the progress of the scan. When I call this GUI alone, the progressbar updates and progresses correctly as long as I use update() after each change in the progressbar. On the other hand, the Text widget is corretly updating even if I do no use update.

However, the progress bar does not update as it should when I call it from the main GUI, while the Text widget updates correctly.

I hope someone can help!

Below is the code for the progressbar GUI. I am using Python 3.6.

from tkinter.filedialog import *
from tkinter.ttk import *


class ScanDirectoryForJPG(Tk):
    """
    Inherited from the Tk class
    """

    def __init__(self, parent, Path=None):
        Tk.__init__(self, parent)
        self.parent = parent
        self.PathDicom = Path
        if self.Path == None:
            self.Path = askdirectory(title='Select a directory to scan')

        self.title('Scan {} for JPG files'.format(self.Path))
        self.status_string = 'Scanning the content of {} folder\n'.format(self.Path)
        self.initialize_gui()
        self.scan_directory()

    def initialize_gui(self):

        # Style
        self.style = Style()
        self.style.theme_use('vista')

        # Main window
        self.grid()
        self.grid_columnconfigure([0], weight=1)
        self.grid_rowconfigure([0], weight=1)

        # Status
        self.status_label = Text(self)
        self.status_label.grid(row=0, column=0, sticky='NSEW')
        self.status_label.insert(END, 'Looking for JPG files in {}\n'.format(self.Path))

        # Progress Bar
        self.p = DoubleVar()
        self.progress_bar = Progressbar(self, orient='horizontal', mode='determinate', variable=self.p, maximum=100)
        self.p.set(0)
        self.progress_bar.grid(row=1, column=0, rowspan=1, sticky='EW')

    def scan_directory(self):
        """

        """
        number_of_files = sum([len(files) for r, d, files in os.walk(self.Path)])
        count = 0

        for dirName, subdirList, fileList in os.walk(self.Path):
            self.status_label.insert(END, '\t-exploring: {}\n'.format(dirName))
            self.update()

            for filename in fileList:
                count += 1
                value = count / number_of_files * self.progress_bar['maximum']
                if value >= (self.progress_bar['value'] + 1):
                    # update the progress bar only when its value is increased by at least 1 (avoid too much updates of the progressbar)
                    self.p.set(self.progress_bar['value'] + 1)
                    self.update()

                file = os.path.join(dirName, filename)

                # if the file is a JPG, load it into the database
                # ...
                # ...
                # ..

        self.status_label.insert(END, 'FINISH\n')
        self.update()


if __name__ == '__main__':
    app = ScanDirectoryForJPG(None, Path='D:\Data\Test')
    app.mainloop()
    print('App closed')

Solution

  • If you have to call update() in tkinter you are doing it wrong.

    In this case you have a loop that walks the directory tree. For the whole time your code is within that loop you are not processing events promptly so you have to call update() all the time.

    Instead, capture the output of the os.walk or simply collect the toplevel directory contents and then use after to process a single item at a time, passing the iterator or list along so once you process a single item, you call after again to process the next one. This way the mainloop will handle UI events promptly with your directory tree processing being queued as events along with everything else. You should fine the application more responsive once you rework it in this manner.

    Example

    To demonstrate this, os.walk returns a generator so we can use after events to schedule each directory as each call of next(generator) yields the next directory with its files.

    To monitor the progress we need some way to count the number of directories or files to be visited and if this demo is used for a whole filesystem that is where the app will appear to freeze. This could be broken into event-based code too to prevent this effect.

    I used after(10, ...) to have it show the effect but for maximum speed, use after_idle instead.

    import sys
    import os
    import tkinter as tk
    import tkinter.ttk as ttk
    from tkinter.filedialog import askdirectory
    
    class App(ttk.Frame):
        def __init__(self, parent, title):
            #tk.Frame.__init__(self, parent)
            super(App, self).__init__(parent)
            parent.wm_withdraw()
            parent.wm_title(title)
            self.create_ui()
            self.grid(sticky = "news")
            parent.wm_protocol("WM_DELETE_WINDOW", self.on_destroy)
            parent.grid_rowconfigure(0, weight=1)
            parent.grid_columnconfigure(0, weight=1)
            parent.wm_deiconify()
    
        def create_ui(self):
            textframe = ttk.Frame(self)
            self.text = text = tk.Text(textframe)
            vs = ttk.Scrollbar(textframe, orient=tk.VERTICAL, command=text.yview)
            text.configure(yscrollcommand=vs.set)
            text.grid(row=0, column=0, sticky=tk.NSEW)
            vs.grid(row=0, column=1, sticky=tk.NS)
    
            textframe.grid_columnconfigure(0, weight=1)
            textframe.grid_rowconfigure(0, weight=1)
            textframe.grid(row=0, column=0, columnspan=2, sticky=tk.NSEW)
            self.progressvar = tk.IntVar()
            self.progress = ttk.Progressbar(self, variable=self.progressvar)
            test_button = ttk.Button(self, text="Walk", command=self.on_walk)
            exit_button = ttk.Button(self, text="Exit", command=self.on_destroy)
            self.progress.grid(row=1, column=0, sticky=tk.NSEW)
            test_button.grid(row=1, column=0, sticky=tk.SE)
            exit_button.grid(row=1, column=1, sticky=tk.SE)
            self.grid_rowconfigure(0, weight=1)
            self.grid_columnconfigure(0, weight=1)
    
        def on_destroy(self):
            self.master.destroy()
    
        def on_walk(self):
            root = askdirectory()
            self.walk(root)
    
        def walk(self, root=None):
            if root:
                # this is potentially costly, but how to find the number of files to be examined?
                count = sum([len(files) for (root,dirs,files) in os.walk(root)])
                self.text.delete("1.0", "end")
                self.progress.configure(maximum=count)
                self.progressvar.set(0)
                walker = os.walk(root)
                self.after(100, self.do_one, walker)
    
        def do_one(self, walker):
            try:
                root,dirs,files = next(walker)
                for file in files:
                    self.text.insert(tk.END, os.path.join(root, file), "PATH", "\n", "")
                    self.text.see(tk.END)
                    self.progressvar.set(self.progressvar.get() + 1)
                self.after(10, self.do_one, walker)
            except StopIteration:
                pass
    
    
    def main(args):
        root = tk.Tk()
        app = App(root, "Walk directory tree")
        root.mainloop()
    
    if __name__ == '__main__':
        sys.exit(main(sys.argv))