Caja de texto numérica (Spinbox) en Tcl/Tk (tkinter)

Caja de texto numérica (Spinbox) en Tcl/Tk (tkinter)



El control ttk.Spinbox es similar a una caja de texto (ttk.Entry), pero incorpora además dos botones para incrementar o disminuir su contenido numérico:

Spinbox en Tk

Aunque este es su uso principal, como contenedor de datos numéricos, puede mostrar otras opciones no numéricas actuando de forma similar a un control ttk.Combobox, uso que mencionaremos al final del artículo.

Para presentar todas las funcionalidades de un spinbox o caja de texto numérica, desarrollaremos un termostato virtual como el siguiente:

Termostato en Tk

El termostato, que está representado por un control ttk.Spinbox, permite seleccionar una temperatura entre 10 y 30 º C, y además indica si el consumo de energía en función de la temperatura es bajo, medio o alto.

Empecemos, entonces, por crear una ventana con los controles correspondientes.

from tkinter import ttk
import tkinter as tk

root = tk.Tk()
root.config(width=300, height=200)
root.title("Termostato virtual")

etiqueta_temp = ttk.Label(text="Temperatura:")
etiqueta_temp.place(x=20, y=30, width=100)

spin_temp = ttk.Spinbox()
spin_temp.place(x=105, y=30, width=70)

root.mainloop()

Aquí creamos la ventana principal (root), el selector de temperatura (spin_temp) y una etiqueta que lo acompaña con un texto (etiqueta_temp).

Si intentamos presionar en los botones para incrementar y disminuir la temperatura, veremos que el texto se mantiene siempre en cero. Esto ocurre porque no hemos indicado aún cuáles son los límites del rango de números que puede contener nuestro spinbox. Para esto especificaremos los argumentos from_ y to al momento de crear una instancia de ttk.Spinbox, que denotan respectivamente el número mínimo y máximo que puede contener el termostato.

# El termostato soporta valores entre 10 y 30 inclusive.
spin_temp = ttk.Spinbox(from_=10, to=30)
spin_temp.place(x=105, y=30, width=70)

Ahora sí, al presionar los botones la temperatura aumenta o disminuye según un intervalo de 1. Si la temperatura está en 10, al presionar el botón para aumentar cambia a 11, luego a 12, y así sucesivamente. Como queremos implementar un termostato, vamos a hacer que el intervalo sea 0,5 vía el argumento increment:

# Indicar que la temperatura debe aumentar y disminuir
# según un intervalo de 0,5.
spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5)
spin_temp.place(x=105, y=30, width=70)

¡Perfecto! Pero habrás notado que hay un problema: el usuario es capaz de escribir cualquier otro valor en el termostato (pues ttk.Spinbox tiene todas las características de una caja de texto), ¡incluso valores no numéricos! Para subsanar este inconveniente podríamos crear una regla de validación, como explicamos en Validar el contenido de una caja de texto en Tcl/Tk (tkinter). No obstante, aquí parece más sencillo simplemente deshabilitar la posibilidad de que el usuario escriba en el termostato: esto es, convertirlo en una caja de texto de solo lectura, para lo cual usaremos la opción state="readonly".

# Deshabilitar la escritura.
spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, state="readonly")
spin_temp.place(x=105, y=30, width=70)

Ahora bien, puesto que queremos que la temperatura del termostato esté medida en grados Celsius (º C), vamos a aplicarle un formato al valor del spinbox. Para esto tenemos la posibilidad de usar el argumento format, que utiliza los mismos códigos de formato (%f, %d, %s, etc.) del lenguaje C y que explicamos en el artículo Formando cadenas de caracteres. Como la temperatura es un número de coma flotante, utilizaremos el formato %.1fºC, lo cual quiere decir: limitar la cantidad de decimales de la temperatura a un dígito y agregar el sufijo «ºC».

spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, state="readonly",
                        format="%.1fºC")
spin_temp.place(x=105, y=30, width=70)

Ya tenemos la estética del termostato en gran medida resuelta. Agreguémosle ahora cierta funcionalidad. Queremos saber cuándo el usuario cambia la temperatura para poder ejecutar un código en respuesta a este evento. Igual que en los botones, para ello usaremos el argumento command, al cual le pasaremos el nombre de una función.

spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, state="readonly",
                        format="%.1fºC", command=cambio_de_temperatura)
spin_temp.place(x=105, y=30, width=70)

Aquí le indicamos a Tk que debe llamar a la función cambio_de_temperatura() cada vez que el usuario presiona en alguno de los dos botones del termostato. Cuando se ejecute esta función, obtendremos la temperatura del termostato y calcularemos el nivel del consumo de energía, que mostraremos en una nueva etiqueta.

from tkinter import ttk
import tkinter as tk


def cambio_de_temperatura():
    temp = float(spin_temp.get()[:2])
    if temp <= 17:
        consumo = "Bajo"
    elif temp <= 24:
        consumo = "Medio"
    elif temp <= 30:
        consumo = "Alto"
    etiqueta_consumo["text"] = f"Consumo de energía: {consumo}."


root = tk.Tk()
root.config(width=300, height=200)
root.title("Termostato virtual")

etiqueta_temp = ttk.Label(text="Temperatura:")
etiqueta_temp.place(x=20, y=30, width=100)

etiqueta_consumo = ttk.Label()
etiqueta_consumo.place(x=20, y=80)

spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, state="readonly",
                        format="%.1fºC", command=cambio_de_temperatura)
spin_temp.place(x=105, y=30, width=70)

root.mainloop()

Detengámonos un momento en la función que acabamos de crear para explicarla.

def cambio_de_temperatura():
    temp = float(spin_temp.get()[:2])
    if temp <= 17:
        consumo = "Bajo"
    elif temp <= 24:
        consumo = "Medio"
    elif temp <= 30:
        consumo = "Alto"
    etiqueta_consumo["text"] = f"Consumo de energía: {consumo}."

Aquí obtenemos primero la temperatura del termostato (spin_temp) vía el método get(), como cualquier otra caja de texto. Ya que en el format habíamos especificado que todo valor debe tener un sufijo «ºC», usando la técnica de slicing de Python quitamos esos últimos dos caracteres con la sintaxis [:2]. Por último, convertimos el texto restante a un número de coma flotante vía la función incorporada float(). Luego, a partir de la temperatura obtenida determinamos si el consumo de energía es bajo, medio o alto, y lo mostramos en la etiqueta_consumo.

Tan solo restaría establecer una temperatura por defecto cuando se inicia el programa. Para insertar un texto en un spinbox usamos insert(), como en las cajas de texto. No obstante, no es posible ejecutar esta función si indicamos que el termostato es de solo lectura vía state="readonly". Para sortear este inconveniente, crearemos el termostato sin esa propiedad, estableceremos el valor inicial y luego deshabilitaremos la escritura.

spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, format="%.1fºC",
                        command=cambio_de_temperatura)
# Establecer el valor inicial.
spin_temp.insert(0, "10ºC")
# Deshabilitar la escritura.
spin_temp["state"] = "readonly"
# Indicar que se alteró la temperatura para actualizar
# el consumo de energía.
cambio_de_temperatura()
spin_temp.place(x=105, y=30, width=70)

Con esto finalizamos el código del termostato, que completo quedaría así:

from tkinter import ttk
import tkinter as tk


def cambio_de_temperatura():
    temp = float(spin_temp.get()[:2])
    if temp <= 17:
        consumo = "Bajo"
    elif temp <= 24:
        consumo = "Medio"
    elif temp <= 30:
        consumo = "Alto"
    etiqueta_consumo["text"] = f"Consumo de energía: {consumo}."


root = tk.Tk()
root.config(width=300, height=200)
root.title("Termostato virtual")

etiqueta_temp = ttk.Label(text="Temperatura:")
etiqueta_temp.place(x=20, y=30, width=100)

etiqueta_consumo = ttk.Label()
etiqueta_consumo.place(x=20, y=80)

spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, format="%.1fºC",
                        command=cambio_de_temperatura)
spin_temp.insert(0, "10ºC")
spin_temp["state"] = "readonly"
cambio_de_temperatura()
spin_temp.place(x=105, y=30, width=70)

root.mainloop()

Otras funcionalidades

Al indicar los límites del valor numérico de nuestro termostato, Tk no permitirá al usuario excederse de dichos valores. No es posible disminuir la temperatura por debajo de los 10ºC ni aumentarla por encima de los 30ºC. Pero si indicamos el argumento wrap=True, cuando el usuario intente disminuir la temperatura por debajo de los 10ºC, Tk llevará el termostato al límite superior (30ºC) e, inversamente, cuando se intente aumentar el valor por encima de los 30ºC, se volverá a los 10ºC.

spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, format="%.1fºC",
                        command=cambio_de_temperatura, wrap=True)

Para determinar cuándo el usuario cambia el valor del termostato usamos el argumento command con una función definida en nuestro código. En algunas circunstancias es útil saber si el usuario presionó el botón para incrementar el valor o el botón para disminuirlo. Para este propósito, Tk brinda la posibilidad de asociar funciones a las señales <<Increment>> y <<Decrement>>. Por ejemplo:

spin_temp.bind("<<Increment>>", temp_incrementada)
spin_temp.bind("<<Decrement>>", temp_disminuida)

Las funciones asociadas con estos dos eventos deben recibir un argumento, que será una instancia de tk.Event, a diferencia de la función asociada con command. Por ejemplo:

def temp_incrementada(event):
    print("Temperatura aumentada.")

def temp_disminuida(event):
    print("Temperatura disminuida.")

Por último, en lugar de usar un spinbox para permitir al usuario seleccionar un valor numérico, podemos proveer una lista de opciones válidas, aun no numéricas. Por ejemplo:

meses = ("Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
         "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre")
spin_meses = ttk.Spinbox(values=meses)

En este caso el funcionamiento es prácticamente igual al de un control ttk.Combobox, con la sola diferencia del modo en que se seleccionan las opciones: en este con una lista desplegable, en el spinbox con los dos botones integrados.



Deja una respuesta