Saltar al contenido
🐍 Curso de Python
/

Módulo 3 · Lección 18 de 50

Los botones de radio: Radiobutton

Elegir una opción entre varias: el grupo que se forma compartiendo una variable, el descuido que deja todos los botones sin marcar, y cuándo conviene un desplegable o una lista en su lugar.

⏱ 18 min de lectura

Un botón de radio nunca va solo. La casilla de la lección anterior era un widget completo por sí misma, con su sí y su no; un Radiobutton aislado no significa nada, porque su razón de ser es ser uno de varios y que solo uno pueda estar marcado. El nombre viene de los botones mecánicos de las radios de coche, donde apretar uno soltaba el que estuviera apretado.

Eso cambia cómo se piensa el widget. Lo que se crea no es un botón, es un grupo, y el grupo no es una caja ni un marco: se forma por compartir la variable. Entender esa frase es entender la lección entera.

El grupo se forma con la variable#

Tres botones, una variable, un value por botón:

python
turno = tk.StringVar(value="Matutino")

for texto in ("Matutino", "Vespertino", "Mixto"):
        ttk.Radiobutton(caja, text=texto, value=texto,
                    variable=turno).pack(anchor="w")

Lo que hace que esos tres sean un grupo es que los tres apuntan a turno. No están agrupados porque estén en el mismo marco ni porque se hayan creado juntos: si crearas un cuarto botón en otra parte de la ventana con la misma variable, entraría en el grupo igual.

El mecanismo es sencillo: un botón se dibuja marcado cuando el valor de la variable coincide con su value. De ahí sale todo lo demás, incluido que solo uno pueda estar marcado, porque la variable solo puede tener un valor a la vez.

consola
>>> turno.get()
'Matutino'
>>> botones["Mixto"].invoke()
>>> turno.get()
'Mixto'
>>> [t for t, b in botones.items() if b.instate(["selected"])]
['Mixto']

Y como el marcado depende de la variable, no hace falta desmarcar nada a mano. Cambiar la variable mueve el grupo entero:

python
turno.set("Vespertino")      # se marca ese y se desmarca el que estuviera

El value puede ser de cualquier tipo, siempre que la variable sea del tipo adecuado:

consola
>>> n = tk.IntVar(value=2)
>>> opciones = {i: ttk.Radiobutton(ventana, text=str(i), value=i, variable=n)
...             for i in (1, 2, 3)}
>>> [i for i, b in opciones.items() if b.instate(["selected"])]
[2]

Con un IntVar y valores numéricos te ahorras convertir después, que es lo cómodo cuando la opción elegida es una cantidad o un identificador.

Los dos descuidos#

Este widget tiene dos formas de salir mal, y las dos son silenciosas.

El primero: ninguna opción marcada. Si el valor inicial de la variable no coincide con ningún value, no se marca nada:

consola
>>> sin_valor = tk.StringVar()          # empieza en cadena vacía
>>> grupo = {t: ttk.Radiobutton(ventana, text=t, value=t, variable=sin_valor)
...          for t in ("A", "B", "C")}
>>> sin_valor.get()
''
>>> any(b.instate(["selected"]) for b in grupo.values())
False

Fíjate en la diferencia con la casilla de la lección anterior. Allí el valor desconocido producía el tercer estado, con su guion; aquí no hay tercer estado que valga:

consola
>>> [t for t, b in grupo.items() if b.instate(["alternate"])]
[]

Simplemente no hay nada marcado, que visualmente es idéntico a "el programa espera que elijas". Y a veces es justo lo que quieres, cuando la opción es obligatoria y no debe haber respuesta por omisión. Lo que no puede pasar es que sea un accidente. Así que decide siempre a propósito, y si quieres un valor por omisión, declara la variable con él:

python
turno = tk.StringVar(value="Matutino")

Disponerlos#

Un grupo se lee mucho mejor en vertical que en horizontal, porque las opciones se comparan de un vistazo recorriendo una columna:

python
for fila, texto in enumerate(OPCIONES):
        ttk.Radiobutton(caja, text=texto, value=texto, variable=eleccion).grid(
        row=fila, column=0, sticky="w", pady=2)

El sticky="w" es imprescindible, como el anchor="w" de la casilla: sin él los botones quedan centrados en la columna y el resultado es un grupo con las marcas desalineadas, que es difícil de leer.

Lo normal es meterlos en un LabelFrame cuyo título sea la pregunta, que es la forma estándar de dejar claro qué opciones pertenecen a qué:

python
caja = ttk.LabelFrame(contenedor, text="Turno", padding=MARGEN)

La alternativa de la etiqueta encima del grupo también vale, pero el LabelFrame dibuja el borde que delimita hasta dónde llega la pregunta, y eso importa cuando hay dos grupos seguidos.

Y un detalle de accesibilidad y de comodidad: el grupo entero ocupa una sola parada del tabulador, y dentro se recorre con las flechas. Eso es cosa de Tk y no hay que programarlo, pero conviene saberlo, porque explica por qué a veces el tabulador "se salta" opciones.

command y trace_add, otra vez#

Vale exactamente lo mismo que en la casilla, y por eso se dice corto. command se llama tras la pulsación y ve el valor nuevo:

consola
>>> registro = []
>>> seleccion = tk.StringVar(value="a")
>>> ra = ttk.Radiobutton(ventana, value="a", variable=seleccion,
...                      command=lambda: registro.append(seleccion.get()))
>>> rb = ttk.Radiobutton(ventana, value="b", variable=seleccion,
...                      command=lambda: registro.append(seleccion.get()))
>>> rb.invoke(); ra.invoke(); rb.invoke()
>>> registro
['b', 'a', 'b']

Y no se llama si la variable cambia desde el código:

consola
>>> seleccion.set("a")
>>> registro
['b', 'a', 'b']

Con el añadido de que aquí hay una razón extra para preferir trace_add: con command tendrías que ponerlo en cada botón del grupo, repitiendo la misma función tres o cinco veces, mientras que trace_add se pone una vez sobre la variable y cubre el grupo entero.

python
turno.trace_add("write", lambda *_: aplicar_turno())

Las opciones#

Un ttk.Radiobutton tiene dieciséis opciones, las mismas que la casilla cambiando onvalue y offvalue por value:

consola
>>> sorted(ttk.Radiobutton(ventana).configure().keys())
['class', 'command', 'compound', 'cursor', 'image', 'justify', 'padding',
 'state', 'style', 'takefocus', 'text', 'textvariable', 'underline', 'value',
 'variable', 'width']

No hay nada nuevo que explicar: text, image y compound funcionan como en el Label, state como en el botón, y style="Toolbutton" convierte el grupo en una fila de botones de los que solo uno queda hundido, que es la forma habitual de elegir entre tres o cuatro vistas en una barra de herramientas.

De métodos, como en la casilla, el temático solo tiene invoke(); select() y deselect() son del clásico y no hacen falta, porque para mover el grupo desde el código se cambia la variable.

Cuándo radio, cuándo desplegable, cuándo lista#

Las tres próximas lecciones resuelven el mismo problema, elegir entre varias opciones, con tres widgets distintos. Conviene tener el criterio antes de conocerlos, porque elegir mal el widget se nota más que cualquier detalle de programación.

WidgetCuántas opcionesSe ven todasElección múltipleCuándo es el adecuado
Radiobutton2 a 5Sí, siempreNoLas opciones importan y hay que compararlas. Ocupa sitio, y eso es su virtud: las opciones están a la vista
CheckbuttonLas que seanSí, siempreSí, independientesVarias opciones que no se excluyen. Cada una es un sí o un no por su cuenta
Combobox5 a cientosNo, hay que abrirloNoHay muchas opciones o poco espacio, o la elección es rutinaria. También si quieres permitir escribir un valor que no está en la lista
ListboxDocenas o másVarias a la vezSí, con selección extendidaHay que ver bastantes de golpe, reordenarlas o elegir varias contiguas

Los dos errores que se ven más a menudo, en ambos sentidos. Un Combobox con tres opciones esconde información que cabía perfectamente en pantalla y obliga a un clic extra para saber qué hay. Y quince botones de radio llenan la ventana de una cosa que en un desplegable habría ocupado una línea.

La frontera práctica está alrededor de cinco: por debajo, botones de radio; por encima, desplegable. Y hay un matiz que decide los casos dudosos: si quien usa el programa necesita comparar las opciones para decidir, tienen que estar a la vista, aunque sean siete.

Un formulario con dos grupos#

radios.py
"""Radiobutton: dos grupos independientes, con estilo Toolbutton y dependencias."""

import tkinter as tk
from tkinter import ttk

MARGEN = 12
AIRE = 4

TURNOS = ("Matutino", "Vespertino", "Mixto")
MODALIDADES = ("Presencial", "Híbrida", "Remota")
VISTAS = ("Lista", "Fichas", "Tabla")

# Días de teletrabajo permitidos según la modalidad elegida.
DIAS = {"Presencial": 0, "Híbrida": 3, "Remota": 5}


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_grupo(contenedor, titulo, opciones, variable):
        """Un LabelFrame con la pregunta por título y sus opciones en columna."""
    caja = ttk.LabelFrame(contenedor, text=titulo, padding=MARGEN)
    caja.pack(side="left", fill="both", expand=True, padx=(0, AIRE))

    for fila, texto in enumerate(opciones):
                ttk.Radiobutton(caja, text=texto, value=texto, variable=variable).grid(
            row=fila, column=0, sticky="w", pady=2)
    return caja


def crear_vista(contenedor):
        """El mismo widget con aspecto de botones de barra: solo uno queda hundido."""
    caja = ttk.LabelFrame(contenedor, text="Vista", padding=MARGEN)
    caja.pack(fill="x", pady=(MARGEN, 0))

    for texto in VISTAS:
                ttk.Radiobutton(caja, text=texto, value=texto, variable=vista,
                        width=8, style="Toolbutton").pack(
            side="left", padx=(0, AIRE))
    return caja


def describir(*_):
        """Se llama cuando cambia cualquiera de las tres variables."""
    dias = DIAS[modalidad.get()]
    resumen.set(f"Turno {turno.get().lower()}, modalidad {modalidad.get().lower()}, "
                f"vista de {vista.get().lower()}.")
    detalle.set(f"{dias} días de teletrabajo a la semana" if dias
                else "Sin teletrabajo")


ventana = tk.Tk()
ventana.title("Condiciones del puesto")
ventana.minsize(520, 340)

# Una variable por pregunta, y todas con valor inicial explícito.
# Variables sueltas y global: en el bloque E esto pasa a ser una clase.
turno = tk.StringVar(value="Matutino")
modalidad = tk.StringVar(value="Híbrida")
vista = tk.StringVar(value="Lista")
resumen = tk.StringVar()
detalle = tk.StringVar()

cuerpo = ttk.Frame(ventana, padding=MARGEN)
cuerpo.pack(fill="both", expand=True)

grupos = ttk.Frame(cuerpo)
grupos.pack(fill="x")
crear_grupo(grupos, "Turno", TURNOS, turno)
crear_grupo(grupos, "Modalidad", MODALIDADES, modalidad)

crear_vista(cuerpo)

ttk.Separator(cuerpo, orient="horizontal").pack(fill="x", pady=MARGEN)
ttk.Label(cuerpo, textvariable=resumen, wraplength=460, justify="left").pack(
    anchor="w")
ttk.Label(cuerpo, textvariable=detalle, foreground="#6E6E73").pack(
    anchor="w", pady=(AIRE, 0))

# Un trace por variable cubre su grupo entero, sin repetir command por botón.
for variable in (turno, modalidad, vista):
        variable.trace_add("write", describir)
describir()

centrar(ventana)
ventana.mainloop()

Lo que sostiene ese programa son tres variables, una por pregunta, cada una con su valor inicial declarado. Los nueve botones se reparten en tres grupos sin que ningún grupo sepa de los otros, y crear_grupo funciona para cualquier pregunta porque recibe la variable como argumento: es la misma idea de una función por bloque del bloque B, aplicada a un patrón que se repite.

Fíjate también en que ningún botón lleva command. Son nueve; con command habría nueve referencias a la misma función. Con tres trace_add, tres líneas.