Módulo 3 · Lección 12 de 50
Diseños que se adaptan
La disciplina de que una ventana aguante cualquier tamaño: quién crece, cómo diagnosticar un redimensionado roto y qué mínimos poner.
Las cuatro lecciones anteriores han explicado las herramientas. Esta explica la disciplina, que es lo que separa una interfaz que funciona de una que solo funciona en el equipo de quien la escribió.
El problema es fácil de enunciar. Tú montas la ventana a un tamaño, la ves bien y das el trabajo por hecho. Pero esa ventana se va a abrir en una pantalla de otra resolución, con otro tamaño de fuente del sistema, en otro idioma cuyos textos son más largos, y quien la use la va a agrandar, a encoger y a maximizar. Un diseño no está terminado hasta que aguanta todo eso.
La buena noticia es que no hace falta nada nuevo: con expand, fill, sticky y el peso ya tienes todo. Lo que falta es el criterio para aplicarlos y un método para diagnosticar cuando no funcionan.
Qué se adapta y qué no#
El primer error es querer que todo crezca. En una ventana bien diseñada, la mayoría de las cosas mantienen su tamaño y solo una o dos absorben el cambio.
La pregunta que hay que hacerse zona por zona es sencilla: si la ventana midiera el doble, ¿tendría sentido que esto midiera el doble?
| Zona | ¿Crece? | Por qué |
|---|---|---|
| Barra de herramientas | No a lo alto, sí a lo ancho | Los botones no mejoran siendo más altos |
| Barra de estado | Igual | Una línea de texto es una línea de texto |
| Panel lateral de navegación | No a lo ancho, sí a lo alto | Los nombres de las secciones no cambian |
| Fila de botones de un diálogo | No | Un botón enorme no es mejor botón |
| Etiquetas de un formulario | No | El texto es el que es |
| Campos de un formulario | Sí a lo ancho | Más ancho significa ver más de lo escrito |
| Tabla, lista, área de texto, lienzo | Sí, en las dos | Más espacio significa ver más datos |
Sale un patrón claro: crece lo que muestra datos de tamaño variable, y no crece lo que muestra controles. Con eso resuelto, casi siempre queda una sola zona que se lleva el crecimiento.
La regla de los tres niveles#
Un redimensionado se configura en tres sitios a la vez, y olvidar uno es el fallo habitual.
Uno, la ventana tiene que permitirlo. Si le has puesto resizable(False, False), no hay nada que hablar.
Dos, cada contenedor tiene que repartir su espacio. Esto es lo que se olvida, porque hay que hacerlo en cada nivel del árbol. El peso y la expansión no se heredan: que la ventana le dé todo el espacio a un marco no significa que ese marco se lo dé a su contenido.
Tres, cada widget tiene que ocupar el espacio que le dan. Una celda que crece con un widget centrado dentro sigue viéndose mal.
Los dos últimos puntos se expresan distinto según el gestor, y conviene tener la equivalencia a mano:
| Dar espacio a la zona | Ocuparlo | |
|---|---|---|
pack | expand=True en el widget | fill="both" en el widget |
grid | weight=1 en la fila o columna del contenedor | sticky="nsew" en el widget |
Fíjate en la asimetría: en pack las dos cosas se piden en la misma llamada, y en grid una se le pide al contenedor y la otra al widget. Es la causa de la mitad de las confusiones.
El método de diagnóstico#
Cuando el redimensionado no funciona, no hay que adivinar. Hay un recorrido que encuentra el problema siempre, y va de fuera hacia dentro.
Empieza por la ventana y baja nivel por nivel. En cada nivel, hazte las dos preguntas: ¿este contenedor reparte espacio hacia sus hijos? ¿Este hijo ocupa el espacio que recibe? El nivel en el que la respuesta sea que no es el nivel roto.
Para verlo en lugar de razonarlo, el truco de los colores de la lección anterior es lo más rápido: pinta cada marco de un color, agranda la ventana y mira qué color deja de crecer. Ese es el nivel del problema.
Estos son los cuatro fallos que cubren casi todos los casos:
| Síntoma | Causa | Arreglo |
|---|---|---|
| Todo se queda arriba a la izquierda y aparece un hueco gris | Nadie reclama el espacio sobrante | Un expand=True, o un weight=1 en fila y columna |
| La zona crece pero el widget de dentro sigue pequeño y centrado | Falta ocupar el espacio recibido | fill="both" o sticky="nsew" |
| El marco exterior crece y su contenido no | Falta el reparto en el nivel interior | Configurar peso o expansión también dentro del marco |
| Todo crece a la vez y el diseño se deforma | Demasiadas zonas con peso | Quitar peso a las que no deben crecer |
Los mínimos, que son la red de seguridad#
Adaptarse no es solo crecer bien: es también no romperse al encoger. Y encoger es donde más se rompen las interfaces, porque nadie lo prueba.
La protección de base es la de la lección 4, y debería estar en todos tus programas:
ventana.minsize(480, 360)Elegir ese número tiene un método. Monta la ventana, encógela a mano hasta el punto justo antes de que algo empiece a verse mal, mira qué tamaño tiene y usa ese. Se puede comprobar imprimiendo la geometría:
ventana.bind("<Configure>", lambda evento: print(ventana.geometry()))Esa línea imprime el tamaño cada vez que cambia mientras arrastras el borde. Es una herramienta de desarrollo, no algo que se deje en el programa final. El evento <Configure> y el mecanismo de bind tienen su lección en el bloque de interacción; aquí se usa como instrumento de medida y ya está.
Además del mínimo global, grid permite mínimos por fila y por columna, que son más finos:
marco.columnconfigure(0, minsize=120)Eso reserva 120 píxeles para la columna de las etiquetas pase lo que pase, lo que impide que los textos se recorten cuando la ventana se estrecha. Es especialmente útil cuando el programa se va a traducir, porque los mismos textos en alemán ocupan bastante más.
Que el contenido también se adapte#
Hasta aquí todo iba de contenedores. Hay además unos cuantos recursos para que el contenido reaccione, y conviene conocerlos.
wraplength en las etiquetas. Por defecto una etiqueta es una sola línea, larguísima si el texto es largo, y fuerza a la ventana a ser ancha. Con wraplength se le da un ancho en píxeles a partir del cual parte el texto en varias líneas:
ttk.Label(marco, text=texto_largo, wraplength=320, justify="left")wrap="word" en las áreas de texto, que es lo que hace que un Text parta por palabras en vez de dejar una línea infinita con barra de desplazamiento horizontal.
Barras de desplazamiento, que son la respuesta honesta cuando el contenido simplemente no cabe. Tienen su lección en el bloque de widgets, pero el criterio es de diseño: antes de estrujar el contenido para que quepa, plantéate si lo correcto no es permitir desplazarse.
Anchos en caracteres. Como viste en la lección 7, el width de los widgets de texto se mide en caracteres, y eso ya es una forma de adaptación: un Entry con width=30 mide más píxeles si el sistema usa una fuente grande, que es exactamente lo que hace falta.
Ventanas que recuerdan su tamaño#
Un detalle pequeño que se agradece mucho: que la aplicación se abra donde y como estaba la última vez. Con lo visto ya se puede hacer, porque geometry() sin argumentos devuelve la cadena completa y geometry(cadena) la restaura.
"""Una ventana que recuerda su tamaño y su posición entre sesiones."""
import json
import tkinter as tk
from pathlib import Path
from tkinter import ttk
AJUSTES = Path("ventana.json")
def cargar(ventana):
"""Restaura la geometría guardada, si existe y es razonable."""
if not AJUSTES.exists():
ventana.geometry("640x420")
return
try:
datos = json.loads(AJUSTES.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
ventana.geometry("640x420")
return
ventana.geometry(datos.get("geometria", "640x420"))
def guardar(ventana):
"""Escribe la geometría actual y cierra."""
AJUSTES.write_text(
json.dumps({"geometria": ventana.geometry()}, indent=2),
encoding="utf-8",
)
ventana.destroy()
ventana = tk.Tk()
ventana.title("Recuerda su tamaño")
ventana.minsize(420, 300)
marco = ttk.Frame(ventana, padding=12)
marco.pack(fill="both", expand=True)
ttk.Label(marco, text="Redimensiona, cierra y vuelve a abrir.").grid(
row=0, column=0, columnspan=2, sticky="w", pady=(0, 12))
ttk.Label(marco, text="Notas:").grid(row=1, column=0, sticky="nw")
notas = tk.Text(marco, wrap="word", height=8)
notas.grid(row=1, column=1, sticky="nsew", padx=(8, 0))
marco.columnconfigure(1, weight=1)
marco.rowconfigure(1, weight=1)
cargar(ventana)
ventana.protocol("WM_DELETE_WINDOW", lambda: guardar(ventana))
ventana.mainloop()Ciérralo con la ventana agrandada y vuelve a abrirlo: aparece igual que la dejaste. Reconocerás tres cosas de lecciones anteriores. El cierre interceptado con protocol, que garantiza que el guardado ocurre por el único camino de salida. El manejo de errores al leer el fichero, que viene del módulo 1 y evita que un JSON corrupto impida abrir el programa. Y el reparto con peso y sticky bien puestos, que es lo que hace que el área de notas absorba el redimensionado.
El resumen operativo del bloque#
Las cinco lecciones de colocación se condensan en una lista corta que conviene tener presente al montar cualquier ventana:
- Divide la ventana en zonas antes de escribir código, y decide para cada una si crece.
- Usa
packpara las franjas ygridpara todo lo demás. - Un solo gestor por contenedor, y un
Framepor medio cuando hagan falta dos. - Configura el reparto en todos los niveles, porque no se hereda.
- Acompaña siempre el reparto con
fillosticky, o la zona crecerá vacía. - Pon un
minsizea la ventana y mínimos a las columnas que lo necesiten. - Prueba maximizado, al mínimo y en medio antes de darlo por bueno.