How do you run your own code alongside Tkinter's event loop?
Use the after
method on the Tk
object:
from tkinter import *
root = Tk()
def task():
print("hello")
root.after(2000, task) # reschedule event in 2 seconds
root.after(2000, task)
root.mainloop()
Here's the declaration and documentation for the after
method:
def after(self, ms, func=None, *args):
"""Call function once after given time.
MS specifies the time in milliseconds. FUNC gives the
function which shall be called. Additional parameters
are given as parameters to the function call. Return
identifier to cancel scheduling with after_cancel."""
Tkinter managing my event loops alongside my mainloop
You already have an infinite loop running, so you shouldn't be trying to add another one. Instead, you can use the after
method to cause a function to be repeatedly called every so often. In your case, you can replace this:
def updateStock(self):
while True:
labelName = str(s) + "Label"
stockPrice = get_quote(s)
self.labelName = Label(self.myContainer1, text = s.upper() + ": " + str(stockPrice))
self.labelName.pack()
time.sleep(10)
... with this:
def updateStock(self):
labelName = str(s) + "Label"
stockPrice = get_quote()
self.labelName = Label(self.myContainer1, text = s.upper() + ": " + str(stockPrice))
self.labelName.pack()
self.after(10000, self.updateStock)
This will get a quote, add a label, then arrange for itself to be called again in 10 seconds (10,000 ms).
However, I doubt that you want to create a new label every 10 seconds, do you? Eventually the window will fill up with labels. Instead, you can create a label once, then update the label in each iteration. For example, create self.label
once in the init, then in the loop you can do:
self.labelName.configure(text=s.upper() + ": " + str(stockPrice))
How can I actually execute a 'while True' loop code while there is a 'mainloop()' in the code?
You should try to avoid this, but you can use a recursion loop.
make a function called "myloop" and repeat it, like so, and then call it just before mainloop:
def myloop():
<code>
root.after(1, myloop) #repeats the function again 1 1000th of a second after it finishes
#----- down by where you're calling mainloop-----
root.after(1, myloop)
root.mainloop()
How exactly does tkinter's mainloop work?
Although trying to rewrite the tkinter
loop seems troublesome, it seems rewriting the asyncio
loop is quite easy, given tkinter
's after
function. The main gist of it is this:
"""Example integrating `tkinter`'s `mainloop` with `asyncio`."""
import asyncio
import tkinter as tk
from typing import Any, Awaitable, TypeVar
T = TypeVar("T")
class AsyncTk(tk.Tk):
"""
A Tk class that can run asyncio awaitables alongside the tkinter application.
Use `root.run_with_mainloop(awaitable)` instead of `root.mainloop()` as a way to run
coroutines alongside it. It functions similarly to using `asyncio.run(awaitable)`.
Alternatively use `await root.async_loop()` if you need to run this in an asynchronous
context. Because this doesn't run `root.mainloop()` directly, it may not behave exactly
the same as using `root.run_with_mainloop(awaitable)`.
"""
is_running: bool
def __init__(self, /, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.is_running = True
def __advance_loop(self, loop: asyncio.AbstractEventLoop, timeout, /) -> None:
"""Helper method for advancing the asyncio event loop."""
# Stop soon i.e. only advance the event loop a little bit.
loop.call_soon(loop.stop)
loop.run_forever()
# If tkinter is still running, repeat this method.
if self.is_running:
self.after(timeout, self.__advance_loop, loop, timeout)
async def async_loop(self, /) -> None:
"""
An asynchronous variant of `root.mainloop()`.
Because this doesn't run `root.mainloop()` directly, it may not behave exactly
the same as using `root.run_with_mainloop(awaitable)`.
"""
# For threading.
self.tk.willdispatch()
# Run initial update.
self.update()
# Run until `self.destroy()` is called.
while self.is_running:
# Let other code run.
# Uses a non-zero sleep time because tkinter should be expected to be slow.
# This decreases the busy wait time.
await asyncio.sleep(tk._tkinter.getbusywaitinterval() / 10_000)
# Run one event.
self.tk.dooneevent(tk._tkinter.DONT_WAIT)
def run_with_mainloop(self, awaitable: Awaitable[T], /, *, timeout: float = 0.001) -> T:
"""
Run an awaitable alongside the tkinter application.
Similar to using `asyncio.run(awaitable)`.
Use `root.run_with_mainloop(awaitable, timeout=...)` to
customize the frequency the asyncio event loop is updated.
"""
if not isinstance(awaitable, Awaitable):
raise TypeError(f"awaitable must be an Awaitable, got {awaitable!r}")
elif not isinstance(timeout, (float, int)):
raise TypeError(f"timeout must be a float or integer, got {timeout!r}")
# Start a new event loop with the awaitable in it.
loop = asyncio.new_event_loop()
task = loop.create_task(awaitable)
# Use tkinter's `.after` to run the asyncio event loop.
self.after(0, self.__advance_loop, loop, max(1, int(timeout * 1000)))
# Run tkinter, which periodically checks
self.mainloop()
# After tkinter is done, wait until `asyncio` is done.
try:
return loop.run_until_complete(task)
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
def destroy(self, /) -> None:
super().destroy()
self.is_running = False
The example application may be fixed up like this:
import asyncio
from random import randrange
import tkinter as tk
def deg_color(deg, d_per_tick, color):
"""Helper function for updating the degree and color."""
deg += d_per_tick
if 360 <= deg:
deg %= 360
color = f"#{randrange(256):02x}{randrange(256):02x}{randrange(256):02x}"
return deg, color
async def rotator(root, interval, d_per_tick):
"""
An example custom method for running code asynchronously
instead of using `tkinter.Tk.after`.
NOTE: Code that can use `tkinter.Tk.after` is likely
preferable, but this may not fit all use-cases and
may sometimes require more complicated code.
"""
canvas = tk.Canvas(root, height=600, width=600)
canvas.pack()
deg = 0
color = 'black'
arc = canvas.create_arc(
100,
100,
500,
500,
style=tk.CHORD,
start=0,
extent=deg,
fill=color,
)
while root.is_running:
deg, color = deg_color(deg, d_per_tick, color)
canvas.itemconfigure(arc, extent=deg, fill=color)
await asyncio.sleep(interval)
def main():
root = AsyncTk()
root.run_with_mainloop(rotator(root, 1/60, 2))
if __name__ == "__main__":
main()
Related Topics
How to Upgrade Python Version to 3.7
Pythonpath Not Working For Sudo on Gnu/Linux (Works For Root)
How to Iterate Over a List in Chunks
Replacements For Switch Statement in Python
Return Value of X = Os.System(..)
Splitting Out the Output of Ps Using Python
Listing Available Devices in Python-Opencv
How to Create a Density Plot in Matplotlib
Converting Epoch Time With Milliseconds to Datetime
Do Python Regular Expressions Have an Equivalent to Ruby'S Atomic Grouping
Hex/Binary String Conversion in Swift
How to Generate All Permutations of a List
Why Does Append() Always Return None in Python
Modifying List While Iterating
Evaluating a Mathematical Expression in a String
Faster Version of 'Pygame.Event.Get()'. Why Are Events Being Missed and Why Are the Events Delayed
Trouble Installing Scipy in Virtualenv on a Amazon Ec2 Linux Micro Instance