Saltar al contenido
🐍 Curso de Python
/

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.

⏱ 17 min de lectura

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 herramientasNo a lo alto, sí a lo anchoLos botones no mejoran siendo más altos
Barra de estadoIgualUna línea de texto es una línea de texto
Panel lateral de navegaciónNo a lo ancho, sí a lo altoLos nombres de las secciones no cambian
Fila de botones de un diálogoNoUn botón enorme no es mejor botón
Etiquetas de un formularioNoEl texto es el que es
Campos de un formularioSí a lo anchoMás ancho significa ver más de lo escrito
Tabla, lista, área de texto, lienzoSí, en las dosMá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 zonaOcuparlo
packexpand=True en el widgetfill="both" en el widget
gridweight=1 en la fila o columna del contenedorsticky="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íntomaCausaArreglo
Todo se queda arriba a la izquierda y aparece un hueco grisNadie reclama el espacio sobranteUn expand=True, o un weight=1 en fila y columna
La zona crece pero el widget de dentro sigue pequeño y centradoFalta ocupar el espacio recibidofill="both" o sticky="nsew"
El marco exterior crece y su contenido noFalta el reparto en el nivel interiorConfigurar peso o expansión también dentro del marco
Todo crece a la vez y el diseño se deformaDemasiadas zonas con pesoQuitar 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:

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

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

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

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

recordar.py
"""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:

  1. Divide la ventana en zonas antes de escribir código, y decide para cada una si crece.
  2. Usa pack para las franjas y grid para todo lo demás.
  3. Un solo gestor por contenedor, y un Frame por medio cuando hagan falta dos.
  4. Configura el reparto en todos los niveles, porque no se hereda.
  5. Acompaña siempre el reparto con fill o sticky, o la zona crecerá vacía.
  6. Pon un minsize a la ventana y mínimos a las columnas que lo necesiten.
  7. Prueba maximizado, al mínimo y en medio antes de darlo por bueno.