Módulo 3 · Lección 22 de 50
El deslizador: Scale
Elegir arrastrando en vez de escribiendo: el widget temático con doce opciones, el valor decimal que nunca pediste, y el único caso del bloque donde el widget clásico es claramente mejor.
Un Scale es una barra con un tirador que se arrastra. Sirve para lo que el Spinbox de la lección anterior no sabe hacer: elegir un valor cuando lo que importa es la proporción y no la cifra. El volumen, el brillo, la opacidad, el nivel de zoom. Nadie pone el volumen «al 63 %»; se sube hasta que suena bien.
Esa es la frase que decide si este widget es el adecuado, y conviene tenerla clara porque el deslizador se usa mal a menudo. Si alguien podría decir de antemano el número que quiere, el deslizador es el widget equivocado: es impreciso por naturaleza, y eso es una virtud cuando la precisión no importa y un defecto en cuanto importa.
Además, esta lección trae algo que no ha pasado en todo el bloque: el widget clásico es mejor que el temático, y no por poco.
Crear y leer#
El rango y la orientación, y ya está:
volumen = ttk.Scale(caja, from_=0, to=100, orient="horizontal", length=200)
volumen.set(35)length es el largo de la barra en píxeles, y hay que ponerlo o el deslizador sale con su tamaño mínimo, que es diminuto. Es una medida en píxeles y no en caracteres, porque aquí no hay texto que medir.
orient acepta dos valores, y Tk los enumera:
>>> ttk.Scale(ventana, orient="z")
_tkinter.TclError: bad orient "z": must be horizontal or verticalPara el vertical hay que saber una cosa que desconcierta: el from_ queda arriba. Así que un deslizador vertical de volumen, donde lo lógico es que el máximo esté arriba, se declara al revés de como se lee:
ttk.Scale(caja, from_=100, to=0, orient="vertical", length=160)Las escalas invertidas funcionan sin problema y el valor se interpreta bien:
>>> invertida = ttk.Scale(ventana, from_=100, to=0)
>>> invertida.set(25)
>>> invertida.get()
25.0Todo es decimal#
Ese 25.0 no es casualidad. Un ttk.Scale trabaja siempre con números en coma flotante:
>>> escala = ttk.Scale(ventana, from_=0, to=100)
>>> escala.get()
0.0
>>> escala.set(50)
>>> escala.get()
50.0
>>> type(escala.get())
<class 'float'>Aunque el rango sea de enteros y el valor lo hayas puesto con un entero, lo que sale es un float. Y arrastrando el tirador no salen valores redondos, salen los que caen bajo el ratón: 37.42857142857143 es un valor perfectamente normal de este widget.
Eso tiene dos consecuencias que hay que tratar.
La primera es la presentación. Nunca muestres el valor crudo de un deslizador en una etiqueta: redondéalo.
def mostrar(valor):
"""El command recibe el valor, y lo recibe como cadena."""
nivel.set(f"{float(valor):.0f} %")La segunda es más traicionera, y es lo que pasa si atas el deslizador a un IntVar creyendo que así se redondea:
El command recibe una cadena#
command en un deslizador funciona distinto que en el botón o la casilla: recibe un argumento, que es el valor nuevo.
>>> registro = []
>>> escala = ttk.Scale(ventana, from_=0, to=100,
... command=lambda valor: registro.append((valor, type(valor).__name__)))
>>> escala.set(42)
>>> registro
[('42.0', 'str')]Mira los dos detalles. El valor llega como cadena, no como número, así que hay que convertirlo con float(). Y una función que no acepte ese argumento falla, lo que explica el error más común del widget:
ttk.Scale(caja, from_=0, to=100, command=actualizar) # actualizar() sin parámetrosSi actualizar no recibe nada, eso lanza un TypeError en cuanto alguien mueve el tirador, no al crearlo. La firma correcta es def actualizar(valor), y si el valor no te interesa, lambda _: actualizar().
La otra cosa que hay que saber es que command se dispara muchísimo: una vez por cada píxel que se mueve el tirador, no una vez al soltarlo. Para actualizar una etiqueta va bien. Para algo que cueste, como redibujar una gráfica o recargar algo, hay que espaciarlo, y la forma de hacerlo se ve en la lección de tareas largas del bloque E.
Y como es costumbre en el bloque, command responde al movimiento y trace_add sobre la variable responde al dato, con lo que si el programa también cambia el deslizador por su cuenta, lo fiable es el segundo.
Las doce opciones, y lo que falta#
Aquí está la sorpresa de la lección. Un ttk.Scale tiene doce opciones, y es con diferencia el widget más pelado del bloque:
>>> sorted(ttk.Scale(ventana).configure().keys())
['class', 'command', 'cursor', 'from', 'length', 'orient', 'state', 'style',
'takefocus', 'to', 'value', 'variable']Eso es todo: el rango, el largo, la orientación, el valor y la variable. No hay forma de mostrar el número, ni de poner marcas, ni de redondear.
El clásico tiene veinticuatro más, y cuatro de ellas son justo las que se echan en falta:
>>> [o for o in ("resolution", "tickinterval", "showvalue", "label")
... if o in ttk.Scale(ventana).configure()]
[]Opción de tk.Scale | Qué hace |
|---|---|
resolution | Redondea a múltiplos de ese número. Con resolution=5, solo salen 0, 5, 10... |
tickinterval | Dibuja marcas numeradas debajo de la barra cada tantas unidades |
showvalue | Muestra el valor actual sobre el tirador. Está activado por defecto |
label | Un rótulo dibujado dentro del propio widget |
resolution es la que cambia el juego, porque además arregla el problema de los decimales de raíz:
>>> clasica = tk.Scale(ventana, from_=0, to=100, orient="horizontal",
... resolution=5, tickinterval=25, label="Volumen")
>>> clasica.set(37)
>>> clasica.get()
35
>>> type(clasica.get())
<class 'int'>Le has pedido 37 y te ha dado 35, que es el múltiplo de cinco más cercano por debajo. Y get() devuelve un entero de verdad, no un float.
Un panel de ajustes#
Este programa tiene las dos familias, para que la diferencia se vea de una vez, y el patrón de la etiqueta sincronizada que hace falta con el temático.
"""Scale: temático con etiqueta, clásico con marcas, y un vertical invertido."""
import tkinter as tk
from tkinter import ttk
MARGEN = 12
AIRE = 4
def centrar(ventana: tk.Misc) -> None:
"""Coloca la ventana en el centro de la pantalla."""
ventana.update_idletasks()
ancho = ventana.winfo_width()
alto = ventana.winfo_height()
x = (ventana.winfo_screenwidth() - ancho) // 2
y = (ventana.winfo_screenheight() - alto) // 2
ventana.geometry(f"+{x}+{y}")
def crear_tematico(contenedor):
"""ttk.Scale: hay que añadir la etiqueta del valor y mantenerla al día."""
caja = ttk.LabelFrame(contenedor, text="ttk.Scale", padding=MARGEN)
caja.pack(fill="x")
fila = ttk.Frame(caja)
fila.pack(fill="x")
ttk.Label(fila, text="Volumen", width=10, anchor="w").pack(side="left")
ttk.Scale(fila, from_=0, to=100, orient="horizontal",
variable=volumen).pack(side="left", fill="x", expand=True,
padx=AIRE)
# width fijo: la cifra cambia sin mover el deslizador de su sitio.
ttk.Label(fila, textvariable=volumen_texto, width=6,
anchor="e").pack(side="left")
fila2 = ttk.Frame(caja)
fila2.pack(fill="x", pady=(AIRE, 0))
ttk.Label(fila2, text="Opacidad", width=10, anchor="w").pack(side="left")
ttk.Scale(fila2, from_=0, to=1, orient="horizontal",
variable=opacidad).pack(side="left", fill="x", expand=True,
padx=AIRE)
ttk.Label(fila2, textvariable=opacidad_texto, width=6,
anchor="e").pack(side="left")
ttk.Label(caja, text="Doce opciones: ni valor a la vista ni redondeo.",
foreground="#6E6E73").pack(anchor="w", pady=(AIRE, 0))
return caja
def crear_clasico(contenedor):
"""tk.Scale: el valor, las marcas y el redondeo vienen de serie."""
caja = ttk.LabelFrame(contenedor, text="tk.Scale", padding=MARGEN)
caja.pack(fill="x", pady=(MARGEN, 0))
tk.Scale(caja, from_=0, to=100, orient="horizontal", variable=calidad,
resolution=5, tickinterval=25, label="Calidad (múltiplos de 5)",
length=300, relief="flat", highlightthickness=0).pack(fill="x")
ttk.Label(caja, text="Redondea, se lee el número y lleva marcas.",
foreground="#6E6E73").pack(anchor="w")
return caja
def crear_vertical(contenedor):
"""Vertical: from_ queda arriba, así que el rango se declara al revés."""
caja = ttk.LabelFrame(contenedor, text="Vertical", padding=MARGEN)
caja.pack(side="left", fill="y", pady=(MARGEN, 0))
ttk.Scale(caja, from_=100, to=0, orient="vertical", length=120,
variable=graves).pack()
ttk.Label(caja, textvariable=graves_texto, width=4, anchor="center").pack(
pady=(AIRE, 0))
return caja
def describir(*_):
"""Formatea las etiquetas y el resumen, redondeando al usar el valor.
Va en un trace_add y no en el command de cada deslizador: así también se
entera de los cambios que haga el programa, que el command se perdería.
"""
volumen_texto.set(f"{volumen.get():.0f} %")
opacidad_texto.set(f"{opacidad.get():.2f}")
graves_texto.set(f"{graves.get():.0f}")
resumen.set(f"Volumen {volumen.get():.0f} %, opacidad {opacidad.get():.2f}, "
f"calidad {calidad.get()}, graves {graves.get():.0f}.")
ventana = tk.Tk()
ventana.title("Ajustes")
ventana.minsize(560, 460)
# DoubleVar con los ttk.Scale: un IntVar haría que el widget y la variable
# dijeran números distintos. El clásico con resolution sí puede usar IntVar.
# Variables sueltas y global: en el bloque E esto pasa a ser una clase.
volumen = tk.DoubleVar(value=35)
opacidad = tk.DoubleVar(value=0.8)
graves = tk.DoubleVar(value=60)
calidad = tk.IntVar(value=75)
volumen_texto = tk.StringVar()
opacidad_texto = tk.StringVar()
graves_texto = tk.StringVar()
resumen = tk.StringVar()
cuerpo = ttk.Frame(ventana, padding=MARGEN)
cuerpo.pack(fill="both", expand=True)
crear_tematico(cuerpo)
crear_clasico(cuerpo)
abajo = ttk.Frame(cuerpo)
abajo.pack(fill="both", expand=True)
crear_vertical(abajo)
ttk.Separator(cuerpo, orient="horizontal").pack(fill="x", pady=MARGEN)
ttk.Label(cuerpo, textvariable=resumen, wraplength=500, justify="left").pack(
anchor="w")
# Un solo trace por variable cubre las etiquetas y el resumen.
for variable in (volumen, opacidad, graves, calidad):
variable.trace_add("write", describir)
describir()
centrar(ventana)
ventana.mainloop()Arrastra los tres y verás la diferencia de trabajo: los temáticos necesitan una etiqueta, alguien que la formatee y un width fijo para que no tiemble, mientras el clásico enseña su número sin que nadie se lo pida. Ese width=6 de las etiquetas es el truco de la lección 13: sin él, pasar del 99 al 100 movería el deslizador entero.
Y fíjate en que ningún deslizador lleva command, aunque esta lección lo haya explicado. El motivo es el de siempre, y aquí tiene una consecuencia visible: si las etiquetas se rellenaran desde el command, bastaría con que el programa cambiara una variable por su cuenta para que la cifra de al lado se quedara con el valor viejo. Con un trace_add por variable, las etiquetas y el resumen se enteran de todo y hay una sola función que formatea.