Treeview in Tk (tkinter)

The tree view (or hierarchical list) is a UI widget that presents data in hierarchical form, although non-hierarchical data is also supported. It can be used as a table widget too. It was introduced in Tk 8.5 and in Python it is provided by the tkinter.ttk standard module.

/images/treeview-in-tk-tkinter/simple-filesystem-explorer-tkinter.png

(See the source code of this simple file explorer below.)

To include a tree view widget in your Tk application, create a an instance of the ttk.Treeview class.

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview()
treeview.pack()
root.mainloop()

Adding and Retrieving Items

Once the treeview object is created, add rows or items via the insert() method:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview()
treeview.insert("", tk.END, text="Item 1")
treeview.pack()
root.mainloop()
/images/treeview-in-tk-tkinter/tkinter-treeview-single-item.png

Since the tree view presents hierarchical data, the first argument passed to insert() is either a parent item or an empty string (""), meaning that the new item has no predecessor.

The second argument is the position (index) which you want to insert the item in, or tk.END to place it at the end of the tree. So insert("", 0, text="Item 1") places the item at the beginning.

The insert() method returns the ID of the created item, usually abbreviated as iid. To create a child item within Item 1, use:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview()
item = treeview.insert("", tk.END, text="Item 1")
treeview.insert(item, tk.END, text="Subitem 1")
treeview.pack()
root.mainloop()
/images/treeview-in-tk-tkinter/tkinter-treeview-subitem.png

Even Subitem 1 might have other items within it, and so on.

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview()
item = treeview.insert("", tk.END, text="Item 1")
subitem = treeview.insert(item, tk.END, text="Subitem 1")
treeview.insert(subitem, tk.END, text="Another item")
treeview.pack()
root.mainloop()

Tk automatically creates an ID for each item, which is just a string in the form I001, I002, etc. You will rarely want to create an ID for a new item yourself; but if necessary, you can do it by specifying the iid parameter.

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview()
my_iid = "unique_id"
treeview.insert("", tk.END, text="Item 1", iid=my_iid)
treeview.pack()
root.mainloop()

This might be useful to get or modify the item's data later. For example, the following code displays the text of the item in a message box when the user presses a button:

import tkinter as tk
from tkinter import messagebox, ttk
def show_info():
    # Get the text of the item whose Id is stored in `my_iid`.
    text = treeview.item(my_iid, option="text")
    # Display it within a message box.
    messagebox.showinfo(title="Item Info", message=text)
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview()
my_iid = "unique_id"
treeview.insert("", tk.END, text="Item 1", iid=my_iid)
treeview.pack()
button = ttk.Button(text="Show info", command=show_info)
button.pack()
root.mainloop()

The item() methods lets you both read and modify item's data. The previous code displays in message box the text property of the item with ID my_iid. If no option is specified, the function returns a dictionary with the item's data.

def show_info():
    # Get the whole info of the item whose ID is stored in `my_iid`.
    info = treeview.item(my_iid)
    # Display it within a message box.
    messagebox.showinfo(title="Item Info", message=str(info))

The info dictionary will be something like:

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

To change any of these values, simply pass it as an argument:

# Change the text of an item.
treeview.item(my_iid, text="New text")

Items within the tree or contained in another item can be obtained via the get_children() method, which returns a tuple of IDs.

import tkinter as tk
from tkinter import ttk
def show_info():
    # Get the whole list of items and subitems.
    treeview_children = treeview.get_children()
    print(treeview_children)
    # Get subitems ("children") of Item 1.
    item_children = treeview.get_children(item)
    print(item_children)
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview()
item = treeview.insert("", tk.END, text="Item 1")
treeview.insert(item, tk.END, text="Subitem 1")
treeview.pack()
button = ttk.Button(text="Show info", command=show_info)
button.pack()
root.mainloop()

treeview_children is a tuple containing the IDs (strings) of every root item (i.e., does not includes subitems) within the tree. The item_children tuple contains every subitem within Item 1 (whose ID is stored in item).

Moving and Deleting Items

It is possible to move an item from one place to another using move(). For example, if you have inserted these items:

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

You can move Item 1 into Item 2 this way:

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

The move() method receives the item to be moved as the first argument. The second argument must be either another item into which you want to place the original item, or "" in order to place it at the root of the tree view. The third argument is the position or row index where the new item is moved.

Tk provides two functions to remove one or more items: delete() and detach(). The difference is that the former removes the passed items both from the widget and from memory. The later just unlinks them from the tree, and they might be re-linked later using move().

# Removes item 2.
treeview.delete(item2)
# Unlinks item 1 (might be re-linked later).
treeview.detach(item1)

After these calls, the exists() method, which lets you know whether an items exists within the tree, returns False for exists(item2) but True for exists(item1).

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

To remove all items from the tree view use:

treeview.delete(*treeview.get_children())

Miscellaneous Functions

The focus() returns the ID of the item that has focus (which may differ from the selected item) or an empty string if there is no focus. To put the focus on a certain item, pass the item ID:

treeview.focus(item)     # Set the focus on item.
print(treeview.focus())  # Retrieve the id of the item that has focus.

The index() method returns the position of the specified item. Note that this position is relative to the parent item. For this reason, two subitems might have the same position if they have different parents.

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

Creating Columns

A tree view can have multiple columns. Thus it is often used as a table widget. For example, if you need to display a hierarchical list of files and folders, the text of each item could be used to show the file name while adding two extra columns to display its size and last modification.

You will first need to configure the Treeview instance with the required columns:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview(columns=("size", "lastmod"))
treeview.pack()
root.mainloop()

Then you will be able to add a new row to represent a file like this:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview(columns=("size", "lastmod"))
treeview.insert(
    "",
    tk.END,
    text="README.txt",
    values=("850 bytes", "18:30")
)
treeview.pack()
root.mainloop()

Thus, you specify that the item text is "README.txt" via the text parameter (which you might think of as the first column), while the values ("850 bytes" and "18:30") of the extra columns (size and lastmod) are passed via the values parameter.

It is also possible to customize the title of each column.

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview(columns=("size", "lastmod"))
treeview.heading("#0", text="File")
treeview.heading("size", text="Size")
treeview.heading("lastmod", text="Last modification")
treeview.insert(
    "",
    tk.END,
    text="README.txt",
    values=("850 bytes", "18:30")
)
treeview.pack()
root.mainloop()
/images/treeview-in-tk-tkinter/tkinter-treeview-with-columns.png

Internally, Tk uses the name #0 to refer to the first column that is created by default and where the item's text is displayed, while size and lastmod are arbitrary names that you choose during the creation of treeview to refer to the extra columns.

To get the value of the columns of a given item, call the set() method (yes, the name is a little confusing).

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

To query the value of a specific column, add a second argument with the column identifier.

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

And to change its value, pass a new value as the third argument:

# Change 18:30 to 19:30.
treeview.set(item, "lastmod", "19:30")

Icons

Tk lets you include an icon image for an item via the image parameter.

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview(columns=("size", "lastmod"))
treeview.heading("#0", text="File")
treeview.heading("size", text="Size")
treeview.heading("lastmod", text="Last modification")
icon = tk.PhotoImage(file="file.png")
treeview.insert(
    "",
    tk.END,
    text="README.txt",
    values=("850 bytes", "18:30"),
    image=icon
)
treeview.pack()
root.mainloop()

The file.png file (which you can download from the ZIP file at the bottom) must be located in the current working directory. If it is stored elsewhere in the file system, pass its full path instead.

Note that due to a Tcl/Tk limitation, a reference to the image (icon) must be maintained. The following code runs fine, but the image is not displayed because the PhotoImage object gets garbage-collected:

treeview.insert(
    "",
    tk.END,
    text="README.txt",
    values=("850 bytes", "18:30"),
    # Error: a reference to the image is not kept in memory.
    image=tk.PhotoImage(file="file.png")
)

You might pass the content of an image instead of a file path to tk.PhotoImage. This is useful when you need to load an image from memory:

# Load image from memory.
# image_content must be a string or a bytes instance.
icon = tk.PhotoImage(data=image_content)

Managing Selection

The selection() method returns a tuple containing the IDs of the selected items, or an empty tuple if there is no selection. For example, the following code set ups a button that displays a message box with the text of the selected item:

import tkinter as tk
from tkinter import messagebox, ttk
def show_selection():
    try:
        # Get the Id of the first selected item.
        item = treeview.selection()[0]
    except IndexError:
        # If the tuple is empty, there is no selected item.
        messagebox.showwarning(
            message="Debe seleccionar un elemento.",
            title="No hay selección"
        )
    else:
        # Get and display the text of the selected item.
        text = treeview.item(item, option="text")
        messagebox.showinfo(message=text, title="Selección")
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview()
treeview.insert("", tk.END, text="Item 1")
treeview.insert("", tk.END, text="Item 2")
treeview.pack()
button = ttk.Button(text="Mostrar selección", command=show_selection)
button.pack()
root.mainloop()

By default, a tree view allows multiple items to be selected. To allow only one selection, the tk.BROWSE value must be passed to the selectmode parameter.

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

Other functions for handling selected items include:

Method

Description

selection_add()

Adds items to the selection.

selection_remove()

Removes items from the selection.

selection_set()

Similar to selection_add(), but removes previously selected items.

selection_toggle()

Toggles the selection of an item.


For example, the following code selects Items 1 and 2 after adding them to the table:

item1 = treeview.insert("", tk.END, text="Item 1")
item2 = treeview.insert("", tk.END, text="Item 2")
treeview.selection_add(item1)
treeview.selection_add(item2)
# Or simply:
# treeview.selection_add((item1, item2))

Events

You might be notified when an item gets selected and, if it contains subitems, when it gets opened or closed, by binding these virtual events: <<TreeviewSelect>>, <<TreeviewOpen>> and <<TreeviewClose>>.

import tkinter as tk
from tkinter import ttk
def item_selected(event):
    print("Selected.")
def item_opened(event):
    print("Opened.")
def item_closed(event):
    print("Closed.")
root = tk.Tk()
root.title("Treeview in Tk")
treeview = ttk.Treeview()
# Create a new tag with certain event hanlders.
treeview.tag_bind("mytag", "<<TreeviewSelect>>", item_selected)
treeview.tag_bind("mytag", "<<TreeviewOpen>>", item_opened)
treeview.tag_bind("mytag", "<<TreeviewClose>>", item_closed)
# Create two items with the previous tag.
item = treeview.insert("", tk.END, text="Item 1", tags=("mytag",))
treeview.insert(item, tk.END, text="Subitem 1", tags=("mytag",))
treeview.pack()
root.mainloop()

In Tk, events need to be associated with a tag (mytag in the previous code), and then an item can be associated with one or more tags to respond to those events.

Simple File Explorer

Simple file and folder browser using ttk.Treeview.

"""
    Simple File System Explorer with Tk
"""
import sys
import tkinter as tk
from pathlib import Path
from tkinter import ttk
class Application(ttk.Frame):
    def __init__(self, window: tk.Tk | tk.Toplevel) -> None:
        super().__init__(window)
        window.title("Simple File System Explorer")
        # show="tree" removes the column header, since we
        # are not using the table feature.
        self.treeview = ttk.Treeview(self, show="tree")
        self.treeview.grid(row=0, column=0, sticky="nsew")
        # Call the item_opened() method each item an item
        # is expanded.
        self.treeview.tag_bind(
            "fstag", "<<TreeviewOpen>>", self.item_opened)
        # Make sure the treeview widget follows the window
        # when resizing.
        for w in (self, window):
            w.rowconfigure(0, weight=1)
            w.columnconfigure(0, weight=1)
        self.grid(row=0, column=0, sticky="nsew")
        # This dictionary maps the treeview items IDs with the
        # path of the file or folder.
        self.fsobjects: dict[str, Path] = {}
        self.file_image = tk.PhotoImage(file="file.png")
        self.folder_image = tk.PhotoImage(file="folder.png")
        # Load the root directory.
        self.load_tree(Path(Path(sys.executable).anchor))
    def safe_iterdir(self, path: Path) -> tuple[Path, ...] | tuple[()]:
        """
        Like `Path.iterdir()`, but do not raise on permission errors.
        """
        try:
            return tuple(path.iterdir())
        except PermissionError:
            print("You don't have permission to read", path)
            return ()
    def get_icon(self, path: Path) -> tk.PhotoImage:
        """
        Return a folder icon if `path` is a directory and
        a file icon otherwise.
        """
        return self.folder_image if path.is_dir() else self.file_image
    def insert_item(self, name: str, path: Path, parent: str = "") -> str:
        """
        Insert a file or folder into the treeview and return the item ID.
        """
        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: Path, parent: str = "") -> None:
        """
        Load the contents of `path` into the treeview.
        """
        for fsobj in self.safe_iterdir(path):
            fullpath = path / fsobj
            child = self.insert_item(fsobj.name, fullpath, parent)
            # Preload the content of each directory within `path`.
            # This is necessary to make the folder item expandable.
            if fullpath.is_dir():
                for sub_fsobj in self.safe_iterdir(fullpath):
                    self.insert_item(sub_fsobj.name, fullpath / sub_fsobj, child)
    def load_subitems(self, iid: str) -> None:
        """
        Load the content of each folder inside the specified item
        into the treeview.
        """
        for child_iid in self.treeview.get_children(iid):
            if self.fsobjects[child_iid].is_dir():
                self.load_tree(self.fsobjects[child_iid],
                            parent=child_iid)
    def item_opened(self, _event: tk.Event) -> None:
        """
        Handler invoked when a folder item is expanded.
        """
        # Get the expanded item.
        iid = self.treeview.selection()[0]
        # If it is a folder, loads its content.
        self.load_subitems(iid)
root = tk.Tk()
app = Application(root)
root.mainloop()

Download: explorer.zip.

Comments