Barra de desplazamiento (Scrollbar) en Tk (tkinter)

Barra de desplazamiento (Scrollbar) en Tk (tkinter)

La barra de desplazamiento es un control de Tk que permite modificar el área visible (viewport) de otros controles. La barra de desplazamiento puede ser vertical u horizontal, y típicamente se añade a controles que muestran múltiples elementos, líneas o columnas, como la lista (tk.Listbox), la vista de árbol (ttk.Treeview) o la caja de texto (tk.Text). En este artículo veremos cómo crear y configurar barras de desplazamiento.

Lista (Listbox) con barra de desplazamiento

Para entender los conceptos alrededor de la barra de desplazamiento, trabajaremos con uno de los controles que permite mostrar múltiples ítems: la lista (tk.Listbox). Los mismos conceptos se aplican a los otros controles que soportan desplazamiento. He aquí un simple programa que crea una ventana y una lista con 100 ítems:

import tkinter as tk

ventana = tk.Tk()
ventana.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Elemento {i}" for i in range(100)))
listbox.pack()
ventana.mainloop()

Al ejecutar el programa, veremos que la lista solo llega a mostrar un puñado de elementos, pero Tk no agrega automáticamente una barra de desplazamiento. Sin embargo, podemos desplazarnos verticalmente en la lista usando el botón del medio del mouse si posicionamos el cursor encima de la lista:

Esto quiere decir que el procedimiento para desplazar el área visible de un control ya está incoporado en Tk, solo debemos crear la barra de desplazamiento y conectarla con el control correspondiente. El área visible de un control, denominada en inglés viewport, es la parte del control que se muestra al usuario. El área visible de un control puede ser igual al área total si el tamaño del control es lo suficientemente grande para mostrar todo el contenido. En la imagen anterior, observamos que la lista tiene un total de 100 elementos, pero el área visible solo llega a mostrar 10 elementos a la vez, por lo cual el área visible representa un décimo (1/10) del área total. Sin embargo, el área visible coincidirá con el área total cuando la lista tenga 10 elementos o menos, o si aumentamos el tamaño de la lista.

Internamente Tk registra cuál es el área visible de cada uno de los controles, para lo cual usa dos rangos: uno para el área visible vertical, otro para la horizontal. Para simplificar, ahora ocupémonos solo de la vertical. Los rangos que controlan el área visible se extienden, como máximo, desde 0.0 hasta 1.0. Cuando el rango es 0.01.0, el área visible coincide con el área total del control, es decir, se muestran todos los elementos. El primer número del rango vertical indica el extremo superior del área total del control, mientras que el segundo número significa el extremo inferior. Así, no bien ejecutamos el código anterior, el área visible de la lista es 0.00.1:

Que el rango sea 0.00.1 implica que solo el primer 10% del área total del control está visible. Por el tamaño que tiene esta lista en particular, el área visible siempre es de un 10%. Sin embargo, el rango cambia a medida que nos desplazamos hacia abajo: tanto el primer número como el segundo aumentan hasta llegar al rango 0.91.0, en el cual se muestran los últimos 10 elementos de la lista. No obstante, la proporción se mantiene en tanto en cuanto el tamaño del control sea siempre el mismo: la diferencia entre el segundo número y el primero siempre es 0.1 (10%). Con este código podemos observar cómo cambia el rango a medida que nos desplazamos en la lista:

import tkinter as tk
from tkinter import ttk

def mostrar_rango(a, b):
    label["text"] = f"El rango es: {a}-{b}"

ventana = tk.Tk()
ventana.geometry("400x300")
listbox = tk.Listbox(yscrollcommand=mostrar_rango)
listbox.insert(tk.END, *(f"Elemento {i}" for i in range(100)))
listbox.pack()
label = ttk.Label()
label.pack()
ventana.mainloop()

Rango del área visible en una lista

Los controles tk.Listbox, tk.Text, tk.Canvas y ttk.Treeview incluyen el argumento yscrollcommand para pasar una función que será llamada cada vez que el área visible del control cambia. Así, nuestra función mostrar_rango() recibe como argumento los dos extremos del rango (a y b) cada vez que el usuario se desplaza por la lista.

Inversamente, usando el método yview() podemos cambiar el área visible de un control vía código. Por ejemplo:

import tkinter as tk

ventana = tk.Tk()
ventana.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Elemento {i}" for i in range(100)))
# Mover el área visible hacia 0.82.
listbox.yview("moveto", 0.82)
listbox.pack()
ventana.mainloop()

El primer argumento "moveto" indica que queremos mover el área visible del control, mientras que el segundo es un número entre 0.0 y 1.0 que indica el primer número del rango del área visible. El segundo número del rango se calcula automáticamente según el tamaño del control. Con el tamaño actual del control, el rango del área visible quedará en 0.820.92. Para verlo mejor, podemos añadir una caja de texto (ttk.Entry) que permita establecer la fracción del área visible de la lista al presionar la tecla Enter:

import tkinter as tk
from tkinter import ttk

ventana = tk.Tk()
ventana.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Elemento {i}" for i in range(100)))
listbox.pack()
entry = ttk.Entry()
entry.bind("<Return>", lambda ev: listbox.yview("moveto", entry.get()))
entry.pack()
ventana.mainloop()

Nótese que el primer número del rango coincide con el número de elemento: si el rango empieza en 0.0, el primer elemento mostrado es el elemento 0; si empieza en 0.25, el elemento 25; y así sucesivamente. Pero esta coincidencia ocurre solo porque tenemos 100 elementos. Si tuviéramos 50 elementos, un rango que empiece en 0.8 mostraría el elemento 40 (porque el 80% de 50 es 40, 50*0.8 == 40); uno que empiece en 0.40, el elemento 20, y así sucesivamente.

Además, el método yview() permite desplazar el área visible de un control en «unidades» (units) cuando el primer argumento es "scroll". La definición exacta de lo que es una unidad depende del control que se esté desplazando. En una lista, una unidad es equivalente a un elemento. Unidades positivas desplazan el área visible hacia abajo, unidades negativas hacia arriba. Así, el siguiente código emplea dos botones para desplazar hacia abajo y hacia arriba el contenido de la lista:

import tkinter as tk
from tkinter import ttk

ventana = tk.Tk()
ventana.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Elemento {i}" for i in range(100)))
listbox.pack()
boton_subir = ttk.Button(
    text="Subir",
    command=lambda: listbox.yview("scroll", "-1", "units")
)
boton_subir.pack()
boton_bajar = ttk.Button(
    text="Bajar",
    command=lambda: listbox.yview("scroll", "1", "units")
)
boton_bajar.pack()
ventana.mainloop()

Lista con botones de desplazamiento

Para el desplazamiento horizontal (por ejemplo, en una tabla con múltiples columnas), el equivalente a yview() es xview(), mientras que el parámetro xscrollcommand cumple el mismo rol que yscrollcommand.

La barra de desplazamiento

Ahora que conocemos los conceptos fundamentales alrededor del desplazamiento del área visible de un control, veamos qué hay de la barra de desplazamiento. Tk provee dos controles con la misma funcionalidad: tk.Scrollbar y ttk.Scrollbar. Para la diferencia entre los módulos tk y ttk, véase Apariencia y estilos de los controles en Tcl/Tk (tkinter). Con todo, lo visto hasta ahora y lo que explicaremos a continuación valen para ambas clases. La barra de desplazamiento es el control preferido para controlar el área visible de una lista, tabla, caja de texto o cualquier otro control que soporte desplazamiento. Puede tener orientación vertical u horizontal y, por lo general, está constituida por dos botones fijos en los extremos y uno arrastrable a través de un canal. Al botón arrastrable se lo conoce en inglés como thumb (pulgar). En algunos sitemas operativos los botones estáticos están ausentes, por lo cual el usuario solo dispone del thumb para usar la barra de desplazamiento.

Barras de desplazamiento horizontal y vertical

La barra de desplazamiento trabaja, igual que los controles de la sección anterior, con un rango entre 0.0 y 1.0 que indica cuáles son la posición y el tamaño del thumb. Cuando el rango es 0.01.0, el thumb ocupa todo el canal de la barra de desplazamiento. A medida que aumenta el primer número del rango, el thumb se desplaza hacia abajo (en barras con orientación vertical) o hacia la derecha (orientación horizontal). La diferencia entre el segundo y el primer número del rango indica cuán grande es el thumb. Así, si establecemos el rango de una barra de desplazamiento en 0.20.5, el tamaño del thumb será el 30% (0.5-0.2 == 0.3) del tamaño del canal de la barra:

import tkinter as tk
from tkinter import ttk

ventana = tk.Tk()
ventana.geometry("400x300")
scrollbar = ttk.Scrollbar(orient=tk.VERTICAL)
scrollbar.set(0.2, 0.5)
scrollbar.place(x=50, y=50, height=200)
ventana.mainloop()

El argumento orient indica la orientación de la barra desplazamiento: tk.VERTICAL o tk.HORIZONTAL. El método set() establece el rango de la barra de desplazamiento. El resultado es el siguiente:

Es fácil ver que los botones estáticos cumplen la función de nuestros botones boton_subir y boton_bajar del último código de la sección anterior. Tal es así, que podemos asociar el evento de presión de esos botones vía el parámetro command y veremos que generan los tres mismos argumentos que le habíamos pasado a listbox.yview() para desplazar el contenido de la lista en unidades:

import tkinter as tk
from tkinter import ttk

ventana = tk.Tk()
ventana.geometry("400x300")
scrollbar = ttk.Scrollbar(orient=tk.VERTICAL, command=print)
scrollbar.set(0.2, 0.5)
scrollbar.place(x=50, y=50, height=200)
ventana.mainloop()

Esta no es ninguna coincidencia: el evento generado por la presión de los botones en los extremos de la barra de desplazamiento está pensado precisamente para conectarse con el método yview() o xview() del control cuya área visible quiere controlarse. Así, con el siguiente código ya tenemos en parte una barra para desplazar elementos de una lista:

import tkinter as tk
from tkinter import ttk

ventana = tk.Tk()
ventana.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Elemento {i}" for i in range(100)))
listbox.place(x=10, y=10, width=200, height=180)
scrollbar = ttk.Scrollbar(orient=tk.VERTICAL, command=listbox.yview)
scrollbar.place(x=220, y=10, height=180)
ventana.mainloop()

Sin embargo, vemos que el thumb de la barra de desplazamiento se mantiene quieto y tiene siempre el mismo tamaño. Eso ocurre porque no le estamos indicando vía scrollbar.set() cuál es el rango del área visible de la lista. Por fortuna, vimos que el constructor de la clase tk.Listbox soporta el parámetro yscrollcommand, que recibe una función que será llamada cada vez que el área visible del control es alterada. Por ende, si conectamos ese parámetro con el método scrollbar.set(), conseguiremos que el thumb de la barra de desplazamiento actualice su tamaño y posición cada vez que cambia el área visible de la lista:

import tkinter as tk
from tkinter import ttk

ventana = tk.Tk()
ventana.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Elemento {i}" for i in range(100)))
listbox.place(x=10, y=10, width=200, height=180)
scrollbar = ttk.Scrollbar(orient=tk.VERTICAL, command=listbox.yview)
listbox.config(yscrollcommand=scrollbar.set)
scrollbar.place(x=220, y=10, height=180)
ventana.mainloop()

¡Excelente! No solo conseguimos que el thumb se actualice cada vez que cambia el área visible de la lista, sino también que el área visible se altere al arrastrar el thumb de la barra de desplazamiento. Esto es posible porque Tk además invoca la función pasada al parámetro command cuando el usuario arrastra el thumb por el canal, pero en este caso pasando como primer argumento "moveto" y como segundo argumento un número entre 0.0 y 1.0 que indica el inicio del rango del área visible. Este era el primer modo en que vimos que el método yview() podía desplazar el área visible de un control.

Mejor organizado con clases

Combinando todas las piezas, podemos implementar un puñado de clases que nos permitan crear listas, vistas de árbol y cajas de texto con barras de desplazamiento horizontal y vertical e insertarlas en una ventana sin desacomodar el resto de los controles.

Lista (tk.Listbox) con barra de desplazamiento horizontal y vertical:

import tkinter as tk
from tkinter import ttk


class ListboxFrame(ttk.Frame):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
        self.vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
        self.listbox = tk.Listbox(
            self,
            xscrollcommand=self.hscrollbar.set,
            yscrollcommand=self.vscrollbar.set
        )
        self.hscrollbar.config(command=self.listbox.xview)
        self.hscrollbar.pack(side=tk.BOTTOM, fill=tk.X)
        self.vscrollbar.config(command=self.listbox.yview)
        self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.listbox.pack()


root = tk.Tk()
root.title("Lista con barras de desplazamiento")
root.geometry("400x300")
listbox_frame = ListboxFrame()
listbox_frame.listbox.insert(
    tk.END,
    *(f"Elemento {i} con un texto muy largo" for i in range(100))
)
listbox_frame.pack()
root.mainloop()

Lista (Listbox) con barra de desplazamiento vertical y horizontal

Vista de árbol (ttk.Treeview):

import pathlib
import sys
import tkinter as tk
from tkinter import ttk


class TreeviewFrame(ttk.Frame):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
        self.vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
        self.treeview = ttk.Treeview(
            self,
            xscrollcommand=self.hscrollbar.set,
            yscrollcommand=self.vscrollbar.set
        )
        self.hscrollbar.config(command=self.treeview.xview)
        self.hscrollbar.pack(side=tk.BOTTOM, fill=tk.X)
        self.vscrollbar.config(command=self.treeview.yview)
        self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.treeview.pack()


root = tk.Tk()
root.geometry("400x300")
treeview_frame = TreeviewFrame()
treeview_frame.pack()
treeview_frame.treeview.config(columns=("name", "size"), show="headings")
treeview_frame.treeview.heading("name", text="Nombre del archivo")
treeview_frame.treeview.heading("size", text="Tamaño")
for file in pathlib.Path(sys.executable).parent.iterdir():
    treeview_frame.treeview.insert(
        "", tk.END, values=(file.name, file.stat().st_size))
root.mainloop()

Vista de árbol (ttk.Treeview) con barra de desplazamiento vertical y horizontal

Caja de texto (tk.Text):

import pathlib
import sys
import tkinter as tk
from tkinter import ttk


class TextFrame(ttk.Frame):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
        self.vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
        self.text = tk.Text(
            self,
            xscrollcommand=self.hscrollbar.set,
            yscrollcommand=self.vscrollbar.set,
            wrap=tk.NONE
        )
        self.hscrollbar.config(command=self.text.xview)
        self.hscrollbar.pack(side=tk.BOTTOM, fill=tk.X)
        self.vscrollbar.config(command=self.text.yview)
        self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.text.pack()


root = tk.Tk()
root.geometry("400x300")
text_frame = TextFrame()
text_frame.text.insert(
    "1.0",
    # Si no funciona, cambiar por la ruta de algún archivo de texto largo.
    (pathlib.Path(sys.executable).parent / "news.txt").read_text("utf8")
)
text_frame.pack()
root.mainloop()

Caja de texto (tk.Text) con barra de desplazamiento vertical y horizontal

Estilos

En algunos sistemas operativos y temas es posible personalizar el estilo de la barra de desplazamiento. Para entender cómo funcionan los estilos y la diferencia entre tk.Scrollbar y ttk.Scrollbar, véase Apariencia y estilos de los controles en Tcl/Tk (tkinter).

Para el control temático ttk.Scrollbar el nombre del estilo es TScrollbar. También se puede configurar el estilo de las barras de desplazamiento verticales u horizontales en particular usando los nombres de los estilos específicos Vertical.TScrollbar y Horizontal.TScrollbar. Ejemplo:

import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.geometry("400x300")
root.title("Scrollbar en Tk")

style = ttk.Style()
style.configure(
    "TScrollbar",
    # Color de la flecha en los botones estáticos.
    arrowcolor="#0000ff",
    # Tamaño de la flecha.
    arrowsize=10,
    # Color de fondo de los botones estáticos y del thumb.
    background="#00ff00",
    # Color del borde.
    bordercolor="#ffffff",
    # Color de la parte oscura del relieve 3D.
    darkcolor="#ff0000",
    # Color de la parte clara del relieve 3D.
    lightcolor="#ff0000",
    # Color del frente.
    foreground="#ffff00",
    # Número de líneas en el thumb.
    gripcount=5,
    # Color del canal.
    troughcolor="#ff00ff"
)

vlabel = ttk.Label(text="Vertical")
vlabel.place(x=50, y=20)
vscrollbar = ttk.Scrollbar(orient=tk.VERTICAL)
vscrollbar.place(x=50, y=50, height=200)
vscrollbar.set("0.0", "0.1")

hlabel = ttk.Label(text="Horizontal")
hlabel.place(x=150, y=70)
hscrollbar = ttk.Scrollbar(orient=tk.HORIZONTAL)
hscrollbar.place(x=150, y=100, width=200)
hscrollbar.set("0.5", "0.6")

root.mainloop()

Téngase en cuenta que:

  • No todos los temas soportan todas las opciones de configuración. Algunos solo están disponibles para determinados temas y no en otros.
  • El tema que Tk usa por defecto en Windows no soporta ninguna personalización del estilo ya que trabaja con controles nativos del sistema operativo.

El control clásico tk.Scrollbar acepta los siguientes argumentos para configuración del estilo:

scrollbar = tk.Scrollbar(
    # Color del thumb y de los controles estáticos cuando
    # están activos (tienen el mouse encima o están siendo presionados).
    activebackground="#ffff00",
    # Color de fondo.
    background="#ff0000",
    # Grosor del borde.
    borderwidth=5,
    # Color del borde.
    highlightbackground="#00ff00",
    # Color del borde cuando el control tiene el foco.
    highlightcolor="#0000ff",
    # Grosor del borde cuando el control tiene el foco.
    highlightthickness=5,
    # Color del canal.
    troughcolor="#ff00ff"
)

Ninguna de estas opciones es reconocida en Windows.

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

2 comentarios.

  1. Hola. Muchas gracias por el conocimiento compartido. Tengo una consulta que no he encontrado solucion, quizas hayas resuelto dicho problema. Te agradeceria la ayuda.

    Tengo un ttk.Scrollbar para un canvas el cual tiene una tabla que va cambiando de tamaño a medida que se le agregan elementos. El problema es que el ttk.Scrollbar no se actualiza al tamaño del canvas y no logra bajar todo el canvas para visualizar los elementos que se agregan.

    Gracias !

Deja una respuesta