Saltar al contenido
🐍 Curso de Python
/

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.

⏱ 18 min de lectura

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á:

python
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:

consola
>>> ttk.Scale(ventana, orient="z")
_tkinter.TclError: bad orient "z": must be horizontal or vertical

Para 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:

python
ttk.Scale(caja, from_=100, to=0, orient="vertical", length=160)

Las escalas invertidas funcionan sin problema y el valor se interpreta bien:

consola
>>> invertida = ttk.Scale(ventana, from_=100, to=0)
>>> invertida.set(25)
>>> invertida.get()
25.0

Todo es decimal#

Ese 25.0 no es casualidad. Un ttk.Scale trabaja siempre con números en coma flotante:

consola
>>> 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.

python
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.

consola
>>> 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:

python
ttk.Scale(caja, from_=0, to=100, command=actualizar)      # actualizar() sin parámetros

Si 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:

consola
>>> 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:

consola
>>> [o for o in ("resolution", "tickinterval", "showvalue", "label")
...  if o in ttk.Scale(ventana).configure()]
[]
Opción de tk.ScaleQué hace
resolutionRedondea a múltiplos de ese número. Con resolution=5, solo salen 0, 5, 10...
tickintervalDibuja marcas numeradas debajo de la barra cada tantas unidades
showvalueMuestra el valor actual sobre el tirador. Está activado por defecto
labelUn 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:

consola
>>> 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.

deslizadores.py
"""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.