Vista de árbol (Treeview) en Tcl/Tk (tkinter)



La vista de árbol (o lista jerárquica) es un elemento gráfico que presenta información de modo jerárquico y, opcionalmente, en forma de grilla (filas y columnas). Fue introducido en Tk 8.5 y en Python es provisto por el módulo ttk.

Explorador de archivos y carpetas en Tcl/Tk (tkinter)

Para crear una vista de árbol, creamos una ventana básica de tkinter y una instancia de la clase ttk.Treeview.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk

class Application(ttk.Frame):
    
    def __init__(self, main_window):
        super().__init__(main_window)
        main_window.title("Vista de árbol en Tkinter")
        
        self.treeview = ttk.Treeview(self)
        self.treeview.pack()
        
        self.pack()

main_window = tk.Tk()
app = Application(main_window)
app.mainloop()

La mayor parte del código que mostraremos a continuación irá luego de la construcción del objeto treeview, entre las líneas 13 y 14.

Añadir y obtener elementos

Una vez creado el objeto self.treeview, añadimos información en forma de ítems o filas vía la función Treeview.insert.

        self.treeview.insert("", tk.END, text="Elemento 1")

Vista de árbol (Treeview) simple en Tcl/Tk (tkinter)

Ya que la vista en árbol presenta información de forma jerárquica, el primer argumento que le pasamos a insert() es un ítem o bien una cadena vacía ("") para indicar que el nuevo ítem no tiene predecesor.

El segundo argumento indica la posición en la que queremos insertar el ítem o tk.END para ubciarlo al final del árbol. Así, insert("", 0, text="Elemento 1") lo ubicaría al comienzo.

La función retorna el ID del ítem creado, generalmente abreviado como iid. De esta forma, podemos agregar un nuevo elemento dentro del Elemento 1:

        item = self.treeview.insert("", tk.END, text="Elemento 1")
        self.treeview.insert(item, tk.END, text="Subelemento 1")

Vista de árbol (Treeview) con dos elementos en Tcl/Tk (tkinter)

Incluso nuestro Subelemento 1 puede tener otros ítems dentro de él; y así sucesivamente.

        item = self.treeview.insert("", tk.END, text="Elemento 1")
        subitem = self.treeview.insert(item, tk.END, text="Subelemento 1")
        self.treeview.insert(subitem, tk.END, text="Otro elemento")

Tk crea automáticamente un ID para cada ítem, en forma de cadena, con el formato I001, I002, etc. Rara vez querrás crear por tu cuenta un ID para un nuevo ítem; pero de ser necesario, puedes hacerlo indicando el parámetro iid.

        my_iid = "id_unico"  # Cualquier cadena es aceptable.
        self.treeview.insert("", tk.END, text="Elemento 1", iid=my_iid)

Esto puede ser en ocasiones útil para obtener o modificar información más adelante sobre dicho ítem.

        # Imprime "Elemento 1".
        print(self.treeview.item(my_iid, option="text"))

La función Treeview.item permite acceder y cambiar los datos de un ítem. El código anterior imprime la propiedad text del ítem con ID my_iid. Si no se especifica ninguna opción, la función retorna un diccionario con toda la información disponible.

        print(self.treeview.item(my_iid))

Esto imprime:

{'image': '', 'open': 0, 'tags': '', 'text': 'Elemento 1', 'values': ''}

Para alterar alguno de estos valores, simplemente se lo pasamos como argumento:

        # Modificar el texto del ítem con ID my_iid.
        self.treeview.item(my_iid, text="Nuevo elemento")

Pueden obtenerse todos los elementos dentro del árbol o bien contenidos en otro elemento con la función get_children, que retorna una tupla de IDs.

        item = self.treeview.insert("", tk.END, text="Elemento 1")
        self.treeview.insert(item, tk.END, text="Subelemento 1")
        # Imprime los elementos del árbol.
        print(self.treeview.get_children())
        # Imprime los elementos dentro del Elemento 1.
        print(self.treeview.get_children(item))

Mover y eliminar elementos

Es posible mover un elemento de un lugar a otro utilizando move(). Por ejemplo, considerando los siguientes dos ítems que se encuentran en la raíz del árbol:

        item1 = self.treeview.insert("", tk.END, text="Elemento 1")
        item2 = self.treeview.insert("", tk.END, text="Elemento 2")

Podemos mover el Elemento 1 dentro del Elemento 2 vía:

        self.treeview.move(item1, item2, tk.END)

La función obtiene el ítem que queremos mover como primer argumento. En segundo lugar, otro elemento dentro del cual queremos colocar el ítem original o bien "" para ubicarlo en la raíz de la lista. El tercer argumento indica la posición.

Para remover uno o varios elementos, Tk provee dos funciones: delete y detach. La diferencia es que la primera elimina completamente los ítems que se le pasan como argumento. La segunda, en cambio, únicamente los desvincula del árbol, pudiendo ser re-vinculados utilizando move().

        # Elimina el elemento 2.
        self.treeview.delete(item2)
        # Desvincula el elemento 1.
        self.treeview.detach(item1)

Luego del código anterior, la función Treeview.exists, que posibilita saber si un elemento existe dentro del árbol, retorna False para exists(item2) pero True para exists(item1).

        print(self.treeview.exists(item2))  # False.
        print(self.treeview.exists(item1))  # True.

Otras funciones

Treeview.focus() retorna el ID del ítem que tiene el foco (que puede diferir del elemento seleccionado) o bien una cadena vacía en caso de no haber ninguno. Para poner el foco sobre un elemento determinado, se pasa su ID como argumento.

        self.treeview.focus(item)  # Pone el foco en item.
        print(self.treeview.focus())  # Retorna el ID de item.

La función Treeview.index devuelve la posición del elemento especificado. Nótese que dicha posición es relativa al elemento padre. Por esta razón, dos ítems pueden tener la misma posición si están ubicados en distintas ramas del árbol.

        item1 = self.treeview.insert("", tk.END, text="Elemento 1")
        item2 = self.treeview.insert("", tk.END, text="Elemento 2")
        print(self.treeview.index(item1))  # 0
        print(self.treeview.index(item2))  # 1

Creando columnas

Un treeview puede tener múltiples columnas. Por ejemplo, supongamos que nuestra vista de árbol muestra una lista jerárquica de archivos y carpetas. El texto de cada ítem correspondería al nombre de un fichero, y podríamos añadir una columna que indique el tamaño y otra, la última modificación.

Primero, es necesario indicarle a la clase Treeview las columnas que requerimos.

        self.treeview = ttk.Treeview(self, columns=("size", "lastmod"))

Luego, añadimos un fichero a la lista con su respectivo nombre e información.

        self.treeview.insert("", tk.END, text="README.txt",
                             values=("850 bytes", "18:30"))

Así, indicamos que el texto del ítem es “README.txt”, y los valores para las columnas size y lastmod son “850 bytes” y “18:30” vía el parámetro values.

También es posible configurar el título de cada columna.

        self.treeview.heading("#0", text="Archivo")
        self.treeview.heading("size", text="Tamaño")
        self.treeview.heading("lastmod", text="Última modificación")

Vista de árbol (Treeview) con columnas en Tcl/Tk (tkinter)

Tk utiliza internamente el nombre #0 para referirse a la primera columna que se crea por defecto y en donde se muestra el texto del ítem, mientras que size y lastmod son nombres arbitrarios que escogimos durante la creación de treeview para hacer referencia a las dos nuevas columnas.

Para obtener el valor de las columnas de un ítem determinado, invocamos la función set (aunque el nombre lo haga parecer contradictorio).

        item = self.treeview.insert("", tk.END, text="README.txt",
                                    values=("850 bytes", "18:30"))
        # Imprime {'lastmod': '18:30', 'size': '850 bytes'}.
        print(self.treeview.set(item))

Para conocer el valor de una columna específica, añadimos un segundo argumento con el identificador.

        # Imprime 18:30.
        print(self.treeview.set(item, "lastmod"))

Por último, para establecer un nuevo valor añadimos un tercer argumento.

        # Cambia 18:30 por 19:30.
        self.treeview.set(item, "lastmod", "19:30")

Obtener y establecer elementos seleccionados

La función Treeview.selection retorna una tupla con los IDs de los elementos seleccionados o bien una cadena vacía ("") en caso de no haber ninguno.

# Mostrar el texto del primer elemento seleccionado.
item = self.treeview.selection()[0]
print(self.treeview.item(item, option="text"))

Por defecto una vista de árbol permite seleccionar múltiples elementos. Para permitir únicamente una selección debe pasarse el valor tk.BROWSE al parámetro selectmode.

        self.treeview = ttk.Treeview(self, selectmode=tk.BROWSE)

Otras funciones para manejar los elementos seleccionados incluyen:

  • selection_add(): añade elementos a la selección.
  • selection_remove(): remueve elementos de la selección.
  • selection_set(): similar a selection_add(), pero remueve los elementos previamente seleccionados.
  • selection_toggle(): cambia la selección de un elemento.

Por ejemplo, el siguiente código selecciona los Elementos 1 y 2:

        self.treeview = ttk.Treeview(self)
        item1 = self.treeview.insert("", tk.END, text="Elemento 1")
        item2 = self.treeview.insert("", tk.END, text="Elemento 2")
        self.treeview.selection_add(item1)
        self.treeview.selection_add(item2)

O bien:

        self.treeview.selection_add((item1, item2))

Incluyendo íconos

Tk permite incluir una imagen como ícono para un ítem vía el parámetro image.

        self.icon = tk.PhotoImage(file="file.png")
        self.treeview.insert("", tk.END, text="README.txt",
                             values=("850 bytes", "18:30"),
                             image=self.icon)

Nótese que por una limitación de Tcl/Tk, debe mantenerse una referencia a la imagen (self.icon). El siguiente código se ejecuta correctamente, pero la imagen no se muestra:

        self.treeview.insert("", tk.END, text="README.txt",
                             values=("850 bytes", "18:30"),
                             image=tk.PhotoImage(file="file.png"))

tk.PhotoImage puede recibir directamente el contenido de una imagen en lugar del nombre de un archivo.

        self.icon = tk.PhotoImage(data=image_content)

Eventos

Es posible conocer cuándo un elemento es seleccionado y, si contiene otros elementos dentro de él, también conocer si se abre o se cierra.

La vista de árbol provee tres eventos virtuales para logarlo: <<TreeviewSelect>>, <<TreeviewOpen>> y <<TreeviewClose>>.

class Application(ttk.Frame):
    
    def __init__(self, main_window):
        super().__init__(main_window)
        main_window.title("Vista de árbol en Tkinter")
        
        self.treeview = ttk.Treeview(self)
        # Crear una nueva etiqueta.
        self.treeview.tag_bind("mytag", "<<TreeviewSelect>>",
                               self.item_selected)
        self.treeview.tag_bind("mytag", "<<TreeviewOpen>>",
                               self.item_opened)
        self.treeview.tag_bind("mytag", "<<TreeviewClose>>",
                               self.item_closed)
        
        # Añadir dos elementos indicando la etiqueta anterior para
        # que respondan a los eventos.
        item = self.treeview.insert("", tk.END, text="Elemento 1",
                                    tags=("mytag",))
        self.treeview.insert(item, tk.END, text="Subelemento 1",
                             tags=("mytag",))
        
        self.treeview.pack()
        
        self.pack()
    
    def item_selected(self, event):
        """Item seleccionado."""
        print("Seleccionado.")
    
    def item_opened(self, event):
        """Item abierto."""
        print("Abierto.")
    
    def item_closed(self, event):
        """Item cerrado."""
        print("Cerrado.")

En tkinter los eventos se asocian a una etiqueta determinada (mytag en el código anterior), y luego cada ítem puede asociarse a una o más etiquetas para responder a aquellos eventos.

Ejemplos

Simple explorador de archivos y carpetas utilizando ttk.Treeview.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk
from os import listdir, sep
from os.path import isdir, join, abspath


class Application(ttk.Frame):
    
    def __init__(self, main_window):
        super().__init__(main_window)
        main_window.title("Explorador de archivos y carpetas")
        
        self.treeview = ttk.Treeview(self)
        self.treeview.grid(row=0, column=0, sticky="nsew")
        
        # Asociar el evento de expansión de un elemento con la
        # función correspondiente.
        self.treeview.tag_bind(
            "fstag", "<<TreeviewOpen>>", self.item_opened)
        
        # Expandir automáticamente.
        for w in (self, main_window):
            w.rowconfigure(0, weight=1)
            w.columnconfigure(0, weight=1)
        
        self.grid(row=0, column=0, sticky="nsew")
        
        # Este diccionario conecta los IDs de los ítems de Tk con
        # su correspondiente archivo o carpeta.
        self.fsobjects = {}
        
        self.file_image = tk.PhotoImage(file="file.png")
        self.folder_image = tk.PhotoImage(file="folder.png")
        
        # Cargar el directorio raíz.
        self.load_tree(abspath(sep))
    
    def listdir(self, path):
        try:
            return listdir(path)
        except PermissionError:
            print("No tienes suficientes permisos para acceder a",
                  path)
            return []
    
    def get_icon(self, path):
        """
        Retorna la imagen correspondiente según se especifique
        un archivo o un directorio.
        """
        return self.folder_image if isdir(path) else self.file_image
    
    def insert_item(self, name, path, parent=""):
        """
        Añade un archivo o carpeta a la lista y retorna el identificador
        del ítem.
        """
        iid = self.treeview.insert(
            parent, tk.END, text=name, tags=("fstag",),
            image=self.get_icon(path))
        self.fsobjects[iid] = path
        return iid
    
    def load_tree(self, path, parent=""):
        """
        Carga el contenido del directorio especificado y lo añade
        a la lista como ítemes hijos del ítem "parent".
        """
        for fsobj in self.listdir(path):
            fullpath = join(path, fsobj)
            child = self.insert_item(fsobj, fullpath, parent)
            if isdir(fullpath):
                for sub_fsobj in self.listdir(fullpath):
                    self.insert_item(sub_fsobj, join(fullpath, sub_fsobj),
                                     child)
        
    def load_subitems(self, iid):
        """
        Cargar el contenido de todas las carpetas hijas del directorio
        que se corresponde con el ítem especificado.
        """
        for child_iid in self.treeview.get_children(iid):
            if isdir(self.fsobjects[child_iid]):
                self.load_tree(self.fsobjects[child_iid],
                               parent=child_iid)
    
    def item_opened(self, event):
        """
        Evento invocado cuando el contenido de una carpeta es abierto.
        """
        iid = self.treeview.selection()[0]
        self.load_subitems(iid)


main_window = tk.Tk()
app = Application(main_window)
app.mainloop()

Descarga: explorer.zip.



Deja un comentario