Python 3 and Tkinter: progress bar does not update during script execution

Asked

Viewed 720 times

0

Hello. I am creating a layout converter using Python 3.6.4 and Tkinter.

Among other things the GUI must have a progress bar that updates its value with each interaction of the conversion process, for example, with each processed line of the file being converted, the progress bar must be updated.

The problem I am facing is that the "program" hangs while running the conversion and the bar is not updated. The bar only updates at the end of the conversion operation.

The basic part of the script is this (I have removed everything that is not relevant and left only the essential to see where the problem is or where I am missing):

import sys
from time import sleep
from tkinter import *
from tkinter.ttk import Progressbar

class Gui:

    def __init__(self):
        self.Window = Tk()
        self.Window.geometry('{0}x{1}'.format(600, 400))

        self.progress = StringVar()
        self.progress.set(0)
        self.progressBar = Progressbar(self.Window, maximum=100, orient=HORIZONTAL, variable=self.progress)
        self.progressBar.grid(row=0, column=0, sticky=W + E)

        self.startButton = Button(self.Window, text='Iniciar', command=self.start)
        self.startButton.grid(row=0, column=2)

    def start(self):
        for t in range(0,100):
            self.progress.set(t)
            sleep(0.1)

    def run(self):
        self.Window.mainloop()
        return 0

if __name__ == '__main__':
    Gui = Gui()
    sys.exit(Gui.run())

I’m a beginner in Python but I have long experience (fifteen years) with PHP and etc.

2 answers

1


The problem you are having is that Tkinter (as well as other graphical toolkits in various languages such as GTK, Qt, etc...) run in a single thread. A graphical program always has a main loop, which is where the framework has the code to collect all the events that arrive at the application, and call the corresponding functions so that they are processed.

Hence you configure your application for when the button is pressed, the method start should be called - and passes control to the mainloop tkinter. When the button is pressed, Tkinter takes the mouse event from the operating system, creates the internal event, and calls the start function -- and this in turn does not return control for Tkinter. Instead it makes 100 updates on self.progress, and only after that returns control. When the start has just run is that Tkinter will process all its internal events from set of progressibar.

Even more, every call to time.sleep, your program stands still, not responding to external events or anything - because the code that does this is in the main loop.

And how to tidy up? Simple: in no graphic program you call the tine.sleep or equivalent function (unless it is integrated into the framework). In the case of Tkinter, the correct one is to call the Window.after saying a method to be called after as many milliseconds. You do this scheduling, and return control to the main loop of Tkinter. Of course the status of the progress bar should be stored in some lgar between a chassis in another - as you are already within a class, just use a specific attribute for this.

class Gui:

    def __init__(self):
        ...
        self.progress_count = 0


    def start(self):
        self.progress.set(self.progress_count)
        self.progress_count += 1
        if self.progress_count < 100:
             self.Window.after(100, self.start)
  • As for the use of time.Sleep(), I used only for example code, but it is not used in production code.

  • yes - but you understood the answer? You call the function updating the progressibar to a fixed time interval, with the method .after - and it checks the attribute of "how complete" (but will not update it, as in this example)

  • The error is you can’t make a loop straight to update the property - you have to return to the mainloop so the work can walk.

  • I captured the essence of what you put (I think), the question is, the code I have is a layout converter. It takes some data (input directory, output directory, conversion format), and when the user clicks on the convert button, the python should take file by file from the input directory, read line by line from the file and convert it into a certain format and write; and still: to each processed file (or could be to each row) update the progress bar. I’m unable to visualize how to do this with Tk.after(). I have to study the subject further!

  • 1

    in which case, I think it might be better to call the update_idletasks even after every x lines. Despite what I wrote, in python we have to keep in mind that "practicality Beats purity". Now, what you can do is keep things separate: keep a 4 =/5 linahs method just to update the progress bar, like the one in the example, using the after and in its main processing use the ;update_idletasks - this way you don’t need to mix the logic of updating the progress bar with the logic of your application’s tasks.

0

After hours of searching last night, I found something that also solved the problem: using self.Window.update_idletasks() inside the loop.

Calls all pending Idle tasks, without Processing any other Events. This can be used to carry out Geometry management and redraw widgets if necessary, without Calling any callbacks.

So the final code (with a few minor adjustments) was like this

class Gui:

    def __init__(self):
        self.Window = Tk()
        self.Window.geometry('{0}x{1}'.format(600, 400))

        self.progress = DoubleVar()
        self.progress.set(0)
        self.progressBar = Progressbar(self.Window, maximum=100, orient=HORIZONTAL, variable=self.progress)
        self.progressBar.grid(row=0, column=0, sticky=W + E)

        self.startButton = Button(self.Window, text='Iniciar', command=self.start)
        self.startButton.grid(row=0, column=2)

    def start(self):
        for t in range(0, 100, 20):
            self.progress.set(t)
            print(t)
            self.Window.update_idletasks()
            sleep(2)
        self.progress.set(100)
        print('Fim...')

    def run(self):
        self.Window.mainloop()
        return 0

if __name__ == '__main__':
    Gui = Gui()
    sys.exit(Gui.run())
  • 1

    In doing so, you "steal" the Tkinter main loop function, (Mainloop) - is not the right way to do it. It makes more sense to do this in a very specialized program where you fat have a main loop (mainloop) yours. - and call the update_idletasks for Tkinter to do all the processing. But if your program is not built that way, you should keep each callback simple, with little code, and no ties - and schedule your re-execution through events (as per exemoplo, with the .after). This is the paradigm not only of Tkinter, but of all graphic libs.

  • Both solutions worked, but I can’t tell which ones performed better (I didn’t benchmark), but I’ll see if I can adapt the production code to make use of . after.

  • The performance difference between the two modes is negligible - nor does it make sense to mention it when using a graphic Toolkit in a very high-level language like Python. The difference is more semantic: let Tkinter work as planned (which is to run your mainloop and choreograph your events) or try to take the place of this main loop, without need and without knowing all of its tasks.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.