问题
I am creating a cryptocurrency exchange API client using Python3.5 and Tkinter. I have several displays that I want to update asynchronously every 10 seconds. I am able to update the displays every 10 seconds using Tk.after()
like in this example
def updateLoans():
offers = dd.loanOffers()
demands = dd.loanDemands()
w.LoanOfferView.delete(1.0, END)
w.LoanDemandView.delete(1.0, END)
w.LoanOfferView.insert(END, offers)
w.LoanDemandView.insert(END, demands)
print('loans refreshed')
root.after(10000, updateLoans)
In order for the after
method to continue to update continuously every 10 seconds the function updateLoans()
needs to be passed as a callable into after()
inside of the function.
Now the part that is stumping me, when I make this function asynchronous with python's new async and await keywords
async def updateLoans():
offers = await dd.loanOffers()
demands = await dd.loanDemands()
w.LoanOfferView.delete(1.0, END)
w.LoanDemandView.delete(1.0, END)
w.LoanOfferView.insert(END, offers)
w.LoanDemandView.insert(END, demands)
print('loans refreshed')
root.after(10000, updateLoans)
The problem here is that I can not await a callable inside of the parameters for the after
method. So I get a runtime warning. RuntimeWarning: coroutine 'updateLoans' was never awaited
.
My initial function call IS placed inside of an event loop.
loop = asyncio.get_event_loop()
loop.run_until_complete(updateLoans())
loop.close()
The display populates just fine initially but never updates.
How can I use Tk.after
to continuously update a tkinter display asynchronously?
回答1:
tk.after
accepts a normal function, not a coroutine. To run the coroutine to completion, you can use run_until_complete
, just as you did the first time:
loop = asyncio.get_event_loop()
root.after(10000, lambda: loop.run_until_complete(updateLoans()))
Also, don't call loop.close()
, since you'll need the loop again.
The above quick fix will work fine for many use cases. The fact is, however, that it will render the GUI completely unresponsive if
updateLoans()
takes a long time due to slow network or a problem with the remote service. A good GUI app will want to avoid this.
While Tkinter and asyncio cannot share an event loop yet, it is perfectly possible to run the asyncio event loop in a separate thread. The main thread then runs the GUI, while a dedicated asyncio thread runs all asyncio coroutines. When the event loop needs to notify the GUI to refresh something, it can use a queue as shown here. On the other hand, if the GUI needs to tell the event loop to do something, it can call call_soon_threadsafe or run_coroutine_threadsafe.
Example code (untested):
gui_queue = queue.Queue()
async def updateLoans():
while True:
offers = await dd.loanOffers()
demands = await dd.loanDemands()
print('loans obtained')
gui_queue.put(lambda: updateLoansGui(offers, demands))
await asyncio.sleep(10)
def updateLoansGui(offers, demands):
w.LoanOfferView.delete(1.0, END)
w.LoanDemandView.delete(1.0, END)
w.LoanOfferView.insert(END, offers)
w.LoanDemandView.insert(END, demands)
print('loans GUI refreshed')
# http://effbot.org/zone/tkinter-threads.htm
def periodicGuiUpdate():
while True:
try:
fn = gui_queue.get_nowait()
except queue.Empty:
break
fn()
root.after(100, periodicGuiUpdate)
# Run the asyncio event loop in a worker thread.
def start_loop():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.create_task(updateLoans())
loop.run_forever()
threading.Thread(target=start_loop).start()
# Run the GUI main loop in the main thread.
periodicGuiUpdate()
root.mainloop()
# To stop the event loop, call loop.call_soon_threadsafe(loop.stop).
# To start a coroutine from the GUI, call asyncio.run_coroutine_threadsafe.
来源:https://stackoverflow.com/questions/49958180/using-async-await-keywords-with-tk-after-method-of-tkinter