La función after() en Tcl/Tk (tkinter)

La función after() en Tcl/Tk (tkinter)

La función tkinter.Tk.after() permite programar la ejecución de una función propia para que se ejecute luego de determinada cantidad de tiempo. No obstante, también puede ser utilizada para indicarle a Tk que ejecute una función cada determinada cantidad de tiempo, que es el uso más habitual. Empecemos por crear una ventana sencilla con una etiqueta (tk.Label):

import tkinter as tk


ventana = tk.Tk()
ventana.title("Ejemplo after() en Tk")
ventana.config(width=400, height=300)

etiqueta1 = tk.Label(text="¡Hola mundo!")
etiqueta1.place(x=100, y=70)

ventana.mainloop()

Supongamos, ahora, que queremos mostrar cada dos segundos un número aleatorio entre 1 y 100 en la etiqueta. El código que en principio se nos viene a la mente es algo como lo siguiente:

import random
import time

while True:
    # Generar un número aleatorio y mostrarlo en la etiqueta.
    numero_aleatorio = random.randint(1, 100)
    etiqueta1.config(text=f"Número aleatorio: {numero_aleatorio}")
    # Esperar dos segundos antes de volver a hacerlo.
    time.sleep(2)

El problema es que, donde sea que coloquemos este código, bloqueará el bucle principal de Tk y la interfaz quedará congelada (sobre este problema, véase también Tareas en segundo plano con Tcl/Tk). Una posible solución consiste en mover el bucle a un segundo hilo de ejecución, para que el hilo principal, en el cual se ejecuta la ventana, pueda seguir con sus tareas normalmente. Esto sería adecuado si el código que movemos al nuevo hilo no tiene que interactuar con la ventana. Pero, en este caso, necesitamos actualizar el texto de la etiqueta1 cada vez que generamos un nuevo número aleatorio. Puesto que Tk corre sobre un único hilo, no es seguro cambiar algún aspecto de la interfaz de usuario desde un hilo secundario. La solución correcta, entonces, vendrá de la mano de after().

Ya que after() nos permite indicarle a Tk que tiene que ejecutar una función nuestra transcurrida determinada cantidad de tiempo, podemos dar un primer paso creando una función con el código correspondiente para generar el número aleatorio y mostrarlo en la etiqueta.

import random

# [...]

def actualizar_etiqueta():
    numero_aleatorio = random.randint(1, 100)
    etiqueta1.config(text=f"Número aleatorio: {numero_aleatorio}")

Y luego programar la llamada a esta función antes de invocar el bucle principal:

# Ejecutar la función actualizar_etiqueta() dentro de dos segundos
# (2000 milisegundos).
ventana.after(2000, actualizar_etiqueta)

Así, el código hasta ahora nos quedaría:

import tkinter as tk
import random


def actualizar_etiqueta():
    numero_aleatorio = random.randint(1, 100)
    etiqueta1.config(text=f"Número aleatorio: {numero_aleatorio}")


ventana = tk.Tk()
ventana.title("Ejemplo after() en Tk")
ventana.config(width=400, height=300)

etiqueta1 = tk.Label(text="¡Hola mundo!")
etiqueta1.place(x=100, y=70)

ventana.after(2000, actualizar_etiqueta)

ventana.mainloop()

Si ejecutamos el programa, veremos que la etiqueta tiene el texto «¡Hola, mundo!» por dos segundos y luego cambia por un número aleatorio. Pero ahora queremos que, nuevamente, después de dos segundos, genere otro número aleatorio, y luego espere otros dos segundos, y genere otro, y así sucesivamente. Es decir, actualizar la etiqueta con un nuevo número aleatorio cada dos segundos. Podemos conseguir este resultado muy sencillamente si agregamos un nuevo after() al final de la función:

def actualizar_etiqueta():
    numero_aleatorio = random.randint(1, 100)
    etiqueta1.config(text=f"Número aleatorio: {numero_aleatorio}")
    # Volver a programar esta función para dentro de dos segundos.
    ventana.after(2000, actualizar_etiqueta)

Ahora sí, nuestra etiqueta se actualiza cada dos segundos:

Para situaciones más complejas es posible indicar qué argumentos queremos que se pasen a nuestra función, y así evitar el uso de variables globales. Por ejemplo, ¿cómo podríamos hacer para, en lugar de reemplazar un número aleatorio por otro, ir sumando los números? Dentro de la función deberíamos saber cuál fue el número aleatorio anterior o, mejor dicho, cuál es la suma de todos los números aleatorios anteriores.

# Por defecto, suma es cero, porque la primera vez que se llama
# a la función no se le pasa ningún argumento.
def actualizar_etiqueta(suma=0):
    numero_aleatorio = random.randint(1, 100)
    # Sumamos el número aleatorio que acabamos de generar a la suma
    # de los números anteriores.
    suma += numero_aleatorio
    etiqueta1.config(text=f"Suma de números aleatorios: {suma}")
    # Pasamos como argumento la suma a la función que se volverá
    # a ejecutar dentro de dos segundos.
    ventana.after(2000, actualizar_etiqueta, suma)

(También pueden usarse los argumentos para pasar controles, como etiqueta1 o la misma ventana, si no se quiere o no se puede acceder a la variable global).

En conclusión, la estructura de la función after() es la siguiente:

tkinter.Tk.after(milisegundos, nombre_funcion, *argumentos)

Es decir, luego de los primeros dos argumentos, todos los valores subsiguientes serán pasados en ese mismo orden a la funcion indicada. Si programamos una función así:

ventana.after(5000, mi_funcion, a, b)

Tk se encargará de ejecutar mi_funcion(a, b) dentro de cinco segundos (5000 milisegundos). Si necesitamos que Tk le pase a mi_funcion algún argumento por nombre, por ejemplo, mi_funcion(c=5), podemos usar functools.partial():

ventana.after(5000, functools.partial(mi_funcion, c=5))

Esto también puede combinarse con argumentos posicionales. Por ejemplo:

# Llamará dentro de cinco segundos a mi_funcion(a, b, c=5).
ventana.after(5000, functools.partial(mi_funcion, c=5), a, b)

Por último, es posible cancelar una función antes de que sea ejecutada vía after_cancel():

id_tarea = ventana.after(2000, actualizar_etiqueta)
# (En algún otro lugar). Cancela la ejecución de actualizar_etiqueta().
ventana.after_cancel(id_tarea)



Deja una respuesta