1

Below is my script on using asyncio to operate with tkinter and its ttk.Progressbar() widget. I derived it after referring to these references (1, 2, 3, 4, 5). I got the progessbar widget to work. But I can't seem to be able to cancel(stop) the asyncio Task object responsible for updating tkinter.Tk() that I have created to replace the usual Tk() event mainloop(). As such, I dont' see a command prompt or >>> prompt after the Tk() window is destroyed. How can I or should I terminate the task object root.update_task? I am using python 3.6. I was hoping loop.shutdown_asyncgens() could end the cancelled task but it did not. Why did it not work? Thank you.

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

import time
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 ):
        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)

        #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):
        print( '\ndef connect_esp( self, event ):' )

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

        async def trackjob( loop ):
            print( '\nasync def trackjob( loop ):' )
            print( 'Job: STARTED' ) 
            start_time = loop.time()
            self.sp_pbar.start( 50 )
            self.sp_pbar.update_idletasks()

            result = await dojob( loop, start_time )
            print( 'result = ', result, type(result) )

            if result:
                self.sp_pbar.stop()
                self.sp_pbar.update_idletasks()
                print( 'Job: ENDED' ) 
                return True
            return False

        try:
            future = self.loop.create_task( trackjob( self.loop ) )
            print( 'future = ', future, type(future))
        except syncio.CancelledError as err:
            print( '_connect_esp(): future is cancelled.' )
            raise
        except asyncio.InvalidStateError as err:
            print( '_connect_esp(): The operation is not allowed in this state..' )
            raise
        except asyncio.TimeoutError as err:
            print( '_connect_esp(): The operation exceeded the given deadline..' )
            raise
        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:
        print( '\nasync def tk_update( self, interval ):' )
        if "application has been destroyed" not in err.args[0]:
            raise
    except asyncio.CancelledError as err:
        print( '\nasync def tk_update( self, interval ):' )
        print('Request to cancel tk_update_task received but may not be done.')
        await asyncio.sleep( interval )
    print( '\nasync def tk_update( interval ):' )
    print( '0 Cancelled = ', root.update_task.cancelled() )
    print('END of def tk_update')


def ask_quit( root, interval=INTERVAL ):
    '''Confirmation to quit application.'''
    print( '\ndef ask_quit( self ):' )
    task=root.update_task
    if tkMessageBox.askokcancel( "Quit","Quit?" ):
        task.cancel()
        print( '1 Cancelled = ', task.cancelled() )
        root.destroy() #Destroy the Tk Window instance.
    print( '2 Cancelled = ', task.cancelled() )


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 ) ) 

    try:
        loop.run_forever()
        print('after loop.run_forever()')
    finally:
        loop.run_until_complete( loop.shutdown_asyncgens() )
        loop.close()
    print( 'Is Loop closed = ', loop.isclosed() )


if __name__ == '__main__':
    main()
Sun Bear
  • 7,594
  • 11
  • 56
  • 102

1 Answers1

2

Use run_until_complete and when you cancel the task you'll drop out of the function. You were using run_forever and not stopping the loop so you never got past that function.

    loop.run_until_complete(root.update_task)
    #loop.run_forever()

Also see the doc for run_forever

loop.run_forever()
    Run the event loop until stop() is called.
MarkReedZ
  • 1,421
  • 4
  • 10
  • Thank you for highlighting the need for `loop.stop()` to end a `run_forever` loop; I had wrongly thought that only `loop.close()` was needed. I have implemented `loop.stop()` at the end of my `.ask_quit()` method while still using `loop.run_forever()`. This was actually demonstrated in documentation's [hello world example](https://docs.python.org/3.6/library/asyncio-eventloop.html#hello-world-with-call-soon). However, I overlooked the use of the `.stop()` method there. – Sun Bear Feb 28 '19 at 03:52
  • 1
    From this encounter, I learnt that **both** `loop.run_forever()` and `loop.run_until_complete(root.update_task)` **can be used to implement `tkinter.Tk()`'s event loop**. The latter worked as you had explained despite the asyncio task `root.update_task.cancelled=False` after `root.update_task.cancel()` was issued. An interesting discovery about using the `loop.run_forever()` approach, after further testing with my script, is that the asyncio loop can be stopped and cancelled w/o even bothering to first cancel the asyncio task `root.update_task`. – Sun Bear Feb 28 '19 at 04:31
  • @Craftables I discovered that when the progressbar is running and the tkinter application is manually exited, a `RunTimeError` exception can occur for a `run_until_complete` event loop while a `run_forever` event loop would not have this issue and the application can exit cleanly. The `run_forever` event loop may be simpler to implement tkinter.Tk()'s event loop. Please see my [answer](https://stackoverflow.com/a/54921428/5722359) to my other related question here. – Sun Bear Feb 28 '19 at 12:42