How to bind async method to a keystroke in Tkinter

2019-05-27 04:25发布

问题:

Consider the following example:

import asyncio
import tkinter as tk

class App(tk.Tk):

    def __init__(self):
        super().__init__()
        self.create_widgets()
        self._configure_bindings() # I believe it is not possible 
                                   # to do this if the method needs 
                                   # to be async as well

    def create_widgets(self):
        pass

    def _configure_bindings(self):
        self.bind('<F5>', self.spam) # what's the proper way?
                                     # does this method need to be async as well?

    async def spam(self, event):
        await self.do_something()

    async def do_something():
        pass

async def run_tk(root):
    try:
        while True:
            root.update()
            await asyncio.sleep(.01)
    except tk.TclError as e:
        if "application has been destroyed" not in e.args[0]:
            raise

if __name__ == '__main__':
    app = App()
    asyncio.get_event_loop().run_until_complete(run_tk(app))

What is the proper way to bind async method to a keystroke in tkinter? I've tried something like:

 self.bind('<F5>', self.spam)
 self.bind('<F5>', await self.spam)
 self.bind('<F5>', await self.spam())
 self.bind('<F5>', lambda event: await self.spam(event))

...and a bunch of other combinations, but to no avail.

回答1:

tkinter itself is asynchronous thanks to event loop, the after method and the bindings.

However, if you trying to stick with asyncio it's also possible, but first let's consider what you tried.

Your first try is obviously a fail, because you trying to call spam as a generic function, when it's a coroutine. Your other tries are more correct than a first, but await coroutine or yield from coroutine can be used to start a coroutine from another coroutine only, so it fails again.

So the proper way of start that beast is a scheduling of its execution with a self-explanatory method ensure_future (or old async, which is just a deprecated alias).

Try this example:

import asyncio
import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self._configure_bindings()

    def _configure_bindings(self):
        self.bind('<F5>', lambda event: asyncio.ensure_future(self.spam(event)))

    async def spam(self, event):
        await self.do_something()
        await asyncio.sleep(2)
        print('%s executed!' % self.spam.__name__)

    async def do_something(self):
        print('%s executed!' % self.do_something.__name__)

async def run_tk(root):
    try:
        while True:
            root.update()
            await asyncio.sleep(.01)
    except tk.TclError as e:
        if "application has been destroyed" not in e.args[0]:
            raise

if __name__ == '__main__':
    app = App()
    asyncio.get_event_loop().run_until_complete(run_tk(app))

Also, I think that it's worth to mention this question, since you use an update method.