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
.
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")
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")
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")
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 aselection_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.
Frank says:
Gracias por el contenido , cuando ejecuto el ejemplo del explorador de archivos se ejecuto pero ala vez me manda este mensaje:
No tienes suficientes permisos para acceder a c:\Archivos de programa
No tienes suficientes permisos para acceder a c:\Documents and Settings
No tienes suficientes permisos para acceder a c:\PerfLogs
¿hay alguna forma de poder preguntar al usuario que le seda estos permisos?
Recursos Python says:
Hola. Al convertir el archivo a un ejecutable, habría que indicar que requiere permisos de administrador. La forma de hacerlo depende de la herramienta que uses (PyInstaller, py2exe, etc.).
Saludos!
José María says:
Aunque el programa explore.pyr funciona correctamente,
la sentencia de la linea 16 :
self.treeview = ttk.Treeview(self)
en vez de self, no debería ser
self.treeview = ttk.Treeview(main_window)
Si estoy equivocado, ¿Me puede explicar en que?.
Una duda más, también en el programa explorer, si se codifica un evento para la contracción de una rama,
¿Se debería de programa dentro de el el borrado de los items que se dejan de mostrar?
Por último, comparto el punto de viata de flipo man, con las adiciones que indica, quedaría casi perfecto.
Muchas gracias
Recursos Python says:
Hola José María. El control
treeview
está dentro deApplication
(que es unttk.Frame
), y este está dentro de la ventana. Por eso indicamos entre paréntesisself
(una referencia aApplication
) y nomain_window
.En cuanto a la otra pregunta, si entiendo bien, no es necesario programar nada para que se contraigan los elementos.
Saludos
José María says:
Disculpe, no acabo de entender su explicación,
Estoy empezado en esto, y observo que siempre que se crea un widget, se hace referencia, en primer lugar, a widget padre que lo va a contener, y asi lo vengo observando en casi todos los ejemplos que he visto.
Con su razonamiento, podría entender, que siempre que se instancia un widget, dentro de una clase, situación muy frecuente, no hace salta referirse al widget padre, basta con self ya que esta contenido en esa clase. Seguro que este razonamiento es erroneo, pero es lo que podría entenderse.
Respecto a la segunda parte, me refería a que al contraer una rama, los recursos que se dedicaron a los items que residian en esa rama, y que ya no hacen falta, se deberían liberar. Seguramente no hace falta porque desde luego observo que en ningún ejemplo se hace esto.
Recursos Python says:
Tu razonamiento es correcto. Se comprende la confusión porque
Application
(al que nos referimos comoself
) está heredando la funcionalidad dettk.Frame
, que es un widget. Esto tiene por objetivo un código más organizado, pero es similar a lo siguiente:main_window = tk.Tk()
app = ttk.Frame(main_window)
treeview = ttk.Treeview(app)
ttk.Frame
no tiene ningún aspecto, simplemente se usa para agrupar conjuntos de widgets relacionados. Es similar a la etiquetadiv
de HTML.Y decís bien, por otro lado, que no es necesario liberar recurso alguno.
Saludos
Jonathan says:
Hola, podrias subir denuevo el zip con los codigos por favor?
Gracias
Recursos Python says:
Hola Jonathan. Gracias por anotarlo, ya arreglamos los enlaces.
Jonathan says:
Muchas gracias, cuanta velocidad jajkajak unos capos
Andres Veloso says:
Hola, gracias por el código, está super! pregunta: es posible añadir tk.CheckBox a cada uno de los elementos en TreeView. Gracias.
Recursos Python says:
Hola Andres. No es posible de forma nativa usando Tk, pero se puede hacer «a mano». En este enlace tenés algunos ejemplos.
Saludos
David says:
Cómo puedo cambiar el tamaño y el color de los elementos qué se encuentran en las columnas?
Flipo Man says:
Muy bueno el tuto. Me hubiera gustado un poco más de detalle de los tag_bind y sus self.item_selected , self.item_opened , self.item_closed. pero solucionó mis dudas al respecto de nombrar la primer columna, y de cómo añadir elementos a la lista. el tuto oficial de tkinter se guarda muchos secretos en cuanto a treeview. gracias!
antser rodriguez says:
cuando uso treeviw y le inserto elemento se encoje y no me deja ver todas columnas, intente ponerle un width pero al parecer solo se puede con las columna y no con el widget en si, podrías decirme como establecerle in tamaño fijo para que no pase esto
Recursos Python says:
Hola, podés pasar por el foro y crear un tema con el código de tu ventana para ver dónde está el problema.