0

I am not able to get the ttk.Progressbar widget to work. May I know what is the issue and how I can fix it?

I know the Progressbar widget is functional; when I comment out self.sp_pbar.stop() the progressbar runs but this happens after time.sleep(5) completes which is not the desired behavior.

import tkinter as tk
import tkinter.ttk as ttk
import time

class App(ttk.Frame):


    def __init__( self, master=None, *args, **kw ):

        super().__init__( master,style='App.TFrame')

        self.master = master
        self.espconnecting = False

        self._set_style()
        self._create_widgets()


    def _set_style( self ):
        print( '\ndef _set_style( self ):' )
        self.style = ttk.Style()
        self.style.configure( 'App.TFrame',  background='pink')
        self.style.configure( 'sp.TFrame',  background='light green')


    def _create_widgets( self ):
        print( '\ndef _create_widgets( self ):' )
        self.sp_frame = ttk.Frame( self, style='sp.TFrame' )
        self.sp_frame.grid(row=0, column=0)

        #self.sp_frame widgets
        self.sp_label1 = ttk.Label( self.sp_frame, text='SP(s):')
        self.sp_label2 = ttk.Label( self.sp_frame, text='ESP(s):')
        self.sp_label3 = ttk.Label( self.sp_frame, )

        self.sp_combox = ttk.Combobox( self.sp_frame, state="readonly",
                                       values=['a','b','c']  )
        self.sp_combox.bind('<<ComboboxSelected>>', self._connect_esp)

        self.sp_pbar = ttk.Progressbar( self.sp_frame, length=200,
                                        mode='indeterminate',
                                        orient=tk.HORIZONTAL, )

        self.sp_label1.grid( row=0, column=0 )
        self.sp_combox.grid( row=0, column=1, padx=[10,0] )
        self.sp_pbar.grid(   row=1, column=0, columnspan=2, sticky='ew' )
        self.sp_label2.grid( row=2, column=0)
        self.sp_label3.grid( row=2, column=1)


    def _connect_esp( self, event=None):
        print( '\ndef connect_esp( self, event=None ):' )
        self._show_conn_progress()
        print("START Connection")
        time.sleep(5) # The code is running a function here which can take some time.  
        print("END Connection")
        self.espconnecting = False


    def _show_conn_progress( self ):
        print( '\ndef _show_conn_progress( self ):' )
        self.espconnecting = True
        self.sp_label3['text']='Connecting.....'
        self.sp_label3.update_idletasks()
        self.sp_pbar.start()
        self._update_conn_progress()


    def _update_conn_progress( self ):
        print( '\ndef _update_conn_progress( self ):' )
        if not self.espconnecting:
            print('connected')
            self.sp_pbar.stop()
            self.sp_label3['text']='Connected'
        else:
            print('connecting')
            self.sp_pbar.update_idletasks()
            self.after(500, self._update_conn_progress) # Call this method after 500 ms.


def main():
    root = tk.Tk()
    root.geometry('300x100+0+24')
    root.rowconfigure(0, weight=1)
    root.columnconfigure(0, weight=1)

    app = App( root )
    app.grid(row=0, column=0, sticky='nsew')

    root.mainloop()

if __name__ == '__main__':
    main()
Sun Bear
  • 7,594
  • 11
  • 56
  • 102
  • time.sleep blocks the main thread. U need to use the 'after' method instead – Henry Yik Jan 30 '19 at 11:52
  • @HenryYik The `time.sleep()` represents the running of another function that takes some time. – Sun Bear Jan 30 '19 at 12:22
  • try this out: https://gist.github.com/MattWoodhead/c7c51cd2beaea33e1b8f5057f7a7d78a – Lukas Jan 30 '19 at 12:39
  • Here under "For contrast, here is the threading version which works perfectly." you can see an working example: https://stackoverflow.com/q/7576310/8980073 You need to do something like this: http://sebastiandahlgren.se/2014/06/27/running-a-method-as-a-background-thread-in-python/ – Lukas Jan 30 '19 at 12:40
  • @Lukas Can you confirm that the use of threading is the only way to get the desire GUI behaviour? That is, the .after method can't be used? I ask because introducing threading does make the process of implementing a Progressbar widget more challenging. Am I correct to say that because my code needs to run two processes concurrently, therefore the `.after` method is not suitable? – Sun Bear Jan 30 '19 at 13:56
  • No, I can not confirm this because it may not work with the threading method as well. But I definitely think that the .after method is not suitable. The progressbar widget is the worst widget in my eyes, it is so bad to use. If you can i would switch to another programming/scripting language, if progressbar is important. – Lukas Jan 30 '19 at 14:14
  • @Lukas I figured out how to use `asyncio` to implement the progressbar widget. It can also be used to handle other concurrent processes within a tkinter GUI. I have submitted an answer. – Sun Bear Feb 28 '19 at 08:44

2 Answers2

0

This is what you currently have in your code:

you set self.espconnecting = False

you call _connect_esp()

which calls _show_conn_progress()

which sets self.espconnecting = True and starts the progressbar self.sp_pbar.start()

and then calls _update_conn_progress()

which checks the value of self.espconnecting. If self.espconnecting is True(which it currently is) connection continues and progress bar keeps rolling as expected. If self.espconnecting is False progress bar is stopped self.sp_pbar.stop()

Before .after() can make it's callback in 500ms, Control is passed back to _connect_esp which sets self.espconnecting = False. Then .after() calls _update_conn_progress() which is meant to keep the bar rolling,

but(Here is your problem): what is the last value of self.espconnecting? =False hence, control branches to self.sp_pbar.stop(), which stops the progrss bar. This is why when you comment that line out your code works as expected, because even if control branches there, there will be nothing to prevent the progress bar from working.

SOLUTION

Do not set self.espconnecting = False in _connect_esp() because before .after() makes it's callback in 500ms, control would have been passed back to _connect_esp() which sets self.espconnecting = False which prevents your progress bar from working.

This means you have to find another means to 'end the connection', once it gets started.

N.B: I really don't see the need for time.sleep(5) in the code.

Here is a possible way to go about the solving it:

...
def __init__( self, master=None, *args, **kw ):

    super().__init__( master,style='App.TFrame')

    self.master = master
    self.espconnecting = False
    self.count=0

    self._set_style()
    self._create_widgets()


def _set_style( self ):
    print( '\ndef _set_style( self ):' )
    self.style = ttk.Style()
    self.style.configure( 'App.TFrame',  background='pink')
    self.style.configure( 'sp.TFrame',  background='light green')


def _create_widgets( self ):
    print( '\ndef _create_widgets( self ):' )
    self.sp_frame = ttk.Frame( self, style='sp.TFrame' )
    self.sp_frame.grid(row=0, column=0)

    #self.sp_frame widgets
    self.sp_label1 = ttk.Label( self.sp_frame, text='SP(s):')
    self.sp_label2 = ttk.Label( self.sp_frame, text='ESP(s):')
    self.sp_label3 = ttk.Label( self.sp_frame, )

    self.sp_combox = ttk.Combobox( self.sp_frame, state="readonly",
                                   values=['a','b','c']  )
    self.sp_combox.bind('<<ComboboxSelected>>', self._connect_esp)

    self.sp_pbar = ttk.Progressbar( self.sp_frame, length=200,
                                    mode='indeterminate',
                                    orient=tk.HORIZONTAL, )

    self.sp_label1.grid( row=0, column=0 )
    self.sp_combox.grid( row=0, column=1, padx=[10,0] )
    self.sp_pbar.grid(   row=1, column=0, columnspan=2, sticky='ew' )
    self.sp_label2.grid( row=2, column=0)
    self.sp_label3.grid( row=2, column=1)


def _connect_esp( self, event=None):
    print( '\ndef connect_esp( self, event=None ):' )
    self._show_conn_progress()
    print("START Connection")
    time.sleep(5)

def end_connection(self):
    print("END Connection")
    self.espconnecting = False


def _show_conn_progress( self ):
    print( '\ndef _show_conn_progress( self ):' )
    self.espconnecting = True
    self.sp_label3['text']='Connecting.....'
    self.sp_label3.update_idletasks()
    self.sp_pbar.start()
    self._update_conn_progress()


def _update_conn_progress( self ):
    print( '\ndef _update_conn_progress( self ):' )
    if not self.espconnecting:
        print('connected')
        self.sp_pbar.stop()
        self.sp_label3['text']='Connected'
    else:
        print('connecting')
        #self.sp_pbar.update_idletasks()
        self.after(500, self._update_conn_progress) # Call this method after 500 ms.
        self.count=self.count + 1
        if self.count==10:
            self.end_connection()


def main():
    root = tk.Tk()
    root.geometry('300x100+0+24')
    root.rowconfigure(0, weight=1)
    root.columnconfigure(0, weight=1)

app = App( root )
app.grid(row=0, column=0, sticky='nsew')

root.mainloop()

if __name__ == '__main__':
    main()
Samuel Kazeem
  • 787
  • 1
  • 8
  • 15
  • The `time.sleep(5)` represents the running of another function that takes some time. The Progressbar is used to indicate on the GUI whether the function is running. – Sun Bear Jan 30 '19 at 12:20
  • `self.espconnecting = False` in `_connect_esp()` method, at the end of the task, is needed to indicate that the process of connecting is completed. This is appropriate. Removing that will lead to erroneous behaviour. – Sun Bear Jan 30 '19 at 13:45
  • You a re right. the point is to find another way to end the connection. I have edited my answer, check it again. – Samuel Kazeem Jan 30 '19 at 14:06
  • Thank you for trying to help. ;) However, I have concluded that the `.after()` method cannot implement a indeterminate progressbar in my scenario. This is because the `time.sleep(5)` method used to simulate a connection simply tells the computer to sleep for 5sec. During which, not even the `.after()` method is allowed to run although it involved very much shorter time interval. The count you had implemented initially fudges some movement in the progressbar. However, there is always a delay prior to movement. Also, if a series of connections is attempted, the fudging fails to work. – Sun Bear Feb 28 '19 at 05:31
  • I have attached an answer showing how to use `asyncio` to implement the progressbar. Hope it can be helpful to you. – Sun Bear Feb 28 '19 at 08:49
0

The tkinter .after() method cannot be used to implement an indeterminate ttk.Progressbar() widget concurrently with another ongoing process. This is because the on-going process, simulated by the time.sleep(5) method, is stalling the tkinter application from issuing another process. During the stall, not even the .after() method can run despite it having a very much shorter wait interval.

As mentioned by @Lukas comments and the references he had shared, an approach to implement an indeterminate ttk.Progressbar() running concurrently with another application process is to use a thread.daemon from python's threading module to manage the concurrency.

Alternatively, python's asyncio infrastructure can be used to implement an indeterminate ttk.Progressbar() running concurrently with another application process. I recently explored this possibility. A caveat to this approach is that the "stalling process", and the activation and termination of the ttk.Progressbar must be written up in separate coroutines.

Below is my script showing how to implement asyncio with tkinter 8.6 and its ttk.Progressbar() widget in Python 3.6.

import tkinter as tk
import tkinter.ttk as ttk
import tkinter.messagebox as tkMessageBox

import asyncio

INTERVAL = 0.05 #seconds

class App(ttk.Frame):


    def __init__( self, master, loop, interval=0.05, *args, **kw ):
        super().__init__( master,style='App.TFrame')
        self.master = master
        self.loop = loop
        self._set_style()
        self._create_widgets()


    def _set_style( self ):
        self.style = ttk.Style()
        self.style.configure( 'App.TFrame',  background='pink')
        self.style.configure( 'sp.TFrame',  background='light green')


    def _create_widgets( self ):
        self.sp_frame = ttk.Frame( self, style='sp.TFrame' )
        self.sp_frame.grid(row=0, column=0)

        #sp_frame widgets
        self.sp_label1 = ttk.Label( self.sp_frame, text='SP(s):')
        self.sp_combox = ttk.Combobox(
            self.sp_frame, state="readonly", values=['a','b','c']  )
        self.sp_combox.bind('<<ComboboxSelected>>', self._connect_esp)
        self.sp_pbar = ttk.Progressbar( self.sp_frame, length=200,
                                        mode='indeterminate',
                                        orient=tk.HORIZONTAL, )
        self.sp_label1.grid( row=0, column=0 )
        self.sp_combox.grid( row=0, column=1, padx=[10,0] )
        self.sp_pbar.grid(   row=1, column=0, columnspan=2, sticky='ew' )


    def _connect_esp( self, event):

        async def dojob( loop, start_time, duration=1 ):
            print( '\nasync def dojob( loop, end_time):' )
            while True:
                duration = 3 #seconds
                t = loop.time()
                delta = t - start_time
                print( 'wait time = {}'.format( delta ) )
                if delta >= duration:
                    break
                await asyncio.sleep( 1 )

        async def trackjob( loop ):
            print( '\nasync def trackjob( loop ):' )
            start_time = loop.time()
            self.sp_pbar.start( 50 )
            self.sp_pbar.update_idletasks()
            print( 'Job: STARTED' ) 
            result = await dojob( loop, start_time )
            print( 'result = ', result, type(result) )
            print( 'Job: ENDED' ) 
            self.sp_pbar.stop()
            self.sp_pbar.update_idletasks()

        try:
            task = self.loop.create_task( trackjob( self.loop ) )
            print( 'task = ', task, type(task))
        except Exception:
            raise


async def tk_update( root, interval=INTERVAL ):
    print( '\nasync def tk_update( interval ):' )
    try:
        while True:
            root.update() #tk update 
            await asyncio.sleep( interval )
    except tk.TclError as err:
        if "application has been destroyed" not in err.args[0]:
            raise


def ask_quit( root, loop, interval=INTERVAL ):
    '''Confirmation to quit application.'''
    if tkMessageBox.askokcancel( "Quit","Quit?" ):
        root.update_task.cancel() #Cancel asyncio task to update Tk()
        root.destroy() #Destroy the Tk Window instance.
        loop.stop() # Stop asyncio loop. This is needed before a run_forever type loop can be closed.


def main():
    loop = asyncio.get_event_loop()

    root = tk.Tk()
    root.geometry('300x100+0+24')
    root.rowconfigure(0, weight=1)
    root.columnconfigure(0, weight=1)
    root.update_task = loop.create_task( tk_update( root ) ) 

    app = App( root, loop )
    app.grid(row=0, column=0, sticky='nsew')
    #root.mainloop() #DO NOT IMPLEMENT; this is replaced by running
                     # tk's update() method in a asyncio loop called loop.
                     # See tk_update() method and root.update_task.

    #Tell Tk window instance what to do before it is destroyed.
    root.protocol("WM_DELETE_WINDOW",
                  lambda :ask_quit( root, loop ) ) 

    try:
        print('start loop.run_forever()')
        loop.run_forever()
    finally:
        loop.run_until_complete( loop.shutdown_asyncgens() )
        loop.close()


if __name__ == '__main__':
    main()

Taking a macro view, it does seem that implementing tkinter within Python's asyncio event loop can facilitate the development of better concurrent GUI applications. I am discovering this myself and hope this attached script can help fellow tkinter users in learning so.

Sun Bear
  • 7,594
  • 11
  • 56
  • 102