Tkinter Designer

← Guías de tkinter

¿Cómo hago un frame con scroll en tkinter?

Un Frame no tiene una vista que desplazar, así que no se le puede poner una barra directamente. Lo que sí funciona es meter el frame dentro de un Canvas, que sí se desplaza, y mantener al canvas informado de cuánto ha crecido el frame.

import tkinter as tk
from tkinter import ttk

root = tk.Tk()

canvas = tk.Canvas(root)
bar = ttk.Scrollbar(root, orient="vertical", command=canvas.yview)
inner = tk.Frame(canvas)                       # tus widgets van aquí dentro

canvas.configure(yscrollcommand=bar.set)
canvas.pack(side="left", fill="both", expand=True)
bar.pack(side="right", fill="y")
canvas.create_window((0, 0), window=inner, anchor="nw")

# sin esto el canvas nunca se entera de lo alto que es el frame
inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))

for i in range(40):
    tk.Label(inner, text=f"fila {i}").pack()

root.mainloop()

Por qué no se desplaza sin el binding de Configure

Un canvas solo se desplaza dentro de su scrollregion, y esa región empieza vacía. Si montas todo sin el binding, el canvas está convencido de que no hay nada que ver: la barra ocupa todo el recorrido y el contenido que queda abajo es sencillamente inalcanzable.

El binding de <Configure> se dispara cada vez que el frame interior cambia de tamaño, y canvas.bbox("all") mide todo lo que hay en el canvas, así que la región crece según agregas filas sin que lleves tú la cuenta de las alturas.

El frame va sobre el canvas, no dentro

create_window es lo que pone el frame sobre el canvas, y no es lo mismo que empaquetar el frame dentro de él. Empaquetar un frame dentro de un canvas parece funcionar y luego se niega a desplazarse, porque un hijo empaquetado lo gestiona el administrador de geometría en vez de ser un elemento del canvas.

Usa anchor="nw" para que el frame empiece arriba a la izquierda; por defecto queda centrado, lo que se ve como un hueco misterioso encima de tu primera fila.

La rueda del ratón es aparte

La rueda no viene conectada. Enlázala para desplazar el canvas, teniendo en cuenta que el evento cambia según la plataforma: Windows y macOS mandan <MouseWheel> con un delta, y X11 manda <Button-4> y <Button-5>.

canvas.bind_all("<MouseWheel>", lambda e: canvas.yview_scroll(int(-e.delta / 120), "units"))

Los toolkits modernos ya lo hacen

CustomTkinter tiene CTkScrollableFrame y ttkbootstrap tiene ScrolledFrame, y los dos son un único widget en el que sueltas cosas: nada de canvas, ni binding, ni región de scroll. Si ya usas cualquiera de los dos, no hay razón para montar a mano la receta de arriba.

import customtkinter as ctk
panel = ctk.CTkScrollableFrame(root, label_text="Filas")

import ttkbootstrap as ttkb
panel = ttkb.ScrolledFrame(root, auto_hide=True)

Hacerlo sin buscarlo

Coloca un Scrollable frame y mete widgets dentro. En CustomTkinter y ttkbootstrap la exportación usa el widget de scroll nativo de cada uno, y en tkinter puro escribe la receta del canvas de arriba con el binding ya puesto, así que el mismo diseño corre en los tres.

Diseña la ventana visualmente y ve el código Python generado sobre la marcha.

Abrir el diseñador de tkinter →

Preguntas relacionadas