How to make ttk.Treeview Editable (Python)

In this tutorial we will explore how to make the Tkinter Treeview Widget, “Editable”. In a previous tutorial, we wrote the following code to represent Product data in a Tabular format using the Treeview widget. But the problem with this approach, is that there is no way of editing the Table from the user’s perspective.

Python
import tkinter as tk
from tkinter import ttk


class ProductTable(tk.Tk):
    def __init__(self, products, **kwargs):
        super().__init__(**kwargs)
        self.products = products
        self.tree = ttk.Treeview(self, columns=("ID", "Name", "Category", "Price"), show="headings")
        self.tree.heading("ID", text="ID")
        self.tree.heading("Name", text="Name")
        self.tree.heading("Category", text="Category")
        self.tree.heading("Price", text="Price")

        # Insert sample data into the Treeview
        for product in self.products:
            self.tree.insert("", "end", values=product)
        self.tree.pack(fill="both", expand=True)

products = [
        ("1001", "Product A", "Category 1", "$10.99"),
        ("1002", "Product B", "Category 2", "$15.49"),
        ("1003", "Product C", "Category 1", "$8.95"),
        ("1004", "Product D", "Category 3", "$12.99"),
        ("1005", "Product E", "Category 2", "$9.99"),
        ("1006", "Product F", "Category 1", "$7.50"),
        ("1007", "Product G", "Category 3", "$11.25"),
        ("1008", "Product H", "Category 2", "$14.99"),
        ("1009", "Product I", "Category 1", "$6.99"),
        ("1010", "Product J", "Category 3", "$10.00")
    ]

app = ProductTable(products)
app.mainloop()
How to make ttk.Treeview Editable

Ideally, we want the user to be able to interact with the Table with the mouse cursor, and edit whichever cell he likes. This is what we will do in this tutorial.


How to make the Treeview Table Editable

The first thing you need to understand, is that there is no native way of making the Treeview widget editable. No special parameter, or function that makes it editable. No.

Instead, what we have to do is come up with our own unique solution to make the Treeview widget editable. Basically we have to make the “edit” functionality ourselves. An innovative solution to this problem, is to create the “illusion” that we are editing the Treeview widget. Let me explain how this is possible.

Here is what we will do.

  1. When the user double-clicks on a Treeview cell, we will record these coordinates (available from the double click event).
  2. We will then use these coordinates to spawn an Entry widget at this location, with the same dimensions as the Treeview cell. The Entry widget will appear over the Treeview cell, hiding it temporarily.
  3. We will then copy the value (programmatically) from the Treeview cell to the Entry widget, and highlight the text in blue (giving it the appearance of selected text).
  4. The user can then modify the text inside the Entry widget, and when he is done, will press the Enter key. This will destroy the Entry widget, but before that, we will copy the new text from the Entry widget which the user modified, and update the Treeview cell with it.

Thus, our edit feature is complete. We will now share the code, piece by piece.


Code

First, we will define a special Entry widget class. This class will inherit from the regular Entry widget, and add a few extra methods and some custom initialization logic in the __init__ method. The code has been well commented to help you understand it.

Python
class EntryPopup(ttk.Entry):
    def __init__(self, parent, iid, column, text, **kw):
        super().__init__(parent, **kw)
        self.tv = parent.tree  # reference to parent window's treeview
        self.iid = iid  # row id
        self.column = column 

        self.insert(0, text) 
        self['exportselection'] = False  # Prevents selected text from being copied to  
                                         # clipboard when widget loses focus
        self.focus_force()  # Set focus to the Entry widget
        self.select_all()   # Highlight all text within the entry widget
        self.bind("<Return>", self.on_return) # Enter key bind
        self.bind("<Control-a>", self.select_all) # CTRL + A key bind
        self.bind("<Escape>", lambda *ignore: self.destroy()) # ESC key bind
        
    def on_return(self, event):
        '''Insert text into treeview, and delete the entry popup'''
        rowid = self.tv.focus()  # Find row id of the cell which was clicked
        vals = self.tv.item(rowid, 'values')  # Returns a tuple of all values from the row with id, "rowid"
        vals = list(vals)  # Convert the values to a list so it becomes mutable
        vals[self.column] = self.get()  # Update values with the new text from the entry widget
        self.tv.item(rowid, values=vals)  # Update the Treeview cell with updated row values
        self.destroy()  # Destroy the Entry Widget
        
    def select_all(self, *ignore):
        ''' Set selection on the whole text '''
        self.selection_range(0, 'end')
        return 'break' # returns 'break' to interrupt default key-bindings

Next, in the __init__ method of the Table class, we add the following line, binding the double left-click to a method that we will define in the next step.

Python
        self.tree.bind("<Double-1>", lambda event: self.onDoubleClick(event))

Now we will define the double click method, which is responsible for spawning the Entry widget at the right location.

Python
    def onDoubleClick(self, event):
            '''Executed, when a row is double-clicked'''
            # close previous popups
            try:  # in case there was no previous popup
                self.entryPopup.destroy()
            except AttributeError:
                pass

            # what row and column was clicked on
            rowid = self.tree.identify_row(event.y)
            column = self.tree.identify_column(event.x)

            # return if the header was double clicked
            if not rowid:
                return

            # get cell position and cell dimensions
            x, y, width, height = self.tree.bbox(rowid, column)
            print(x, y, width, height)

            # y-axis offset
            pady = height // 2

            # place Entry Widget
            text = self.tree.item(rowid, 'values')[int(column[1:])-1]
            self.entryPopup = EntryPopup(self, rowid, int(column[1:])-1, text)
            self.entryPopup.place(x=x, y=y+pady, width=width, height=height, anchor='w')

And that’s all. We can now run our code, and double click any cell to edit its values.

We have updated the price of Product-C to $9.99 from its old value $8.95. The best part about this, is that it looks 100% natural. Here is the complete code for the Table class (excluding the custom Entry class).

Python
import tkinter as tk
from tkinter import ttk

class Table(tk.Tk):
    def __init__(self, data, **kwargs):
        super().__init__(**kwargs)
        self.data = data
        self.initialize_ui()
 
    def initialize_ui(self):
        # Create Treeview widget
        self.tree = ttk.Treeview(self, columns=("ID", "Name", "Category", "Price"), show="headings")
        self.tree.heading("ID", text="ID")
        self.tree.heading("Name", text="Name")
        self.tree.heading("Category", text="Category")
        self.tree.heading("Price", text="Price")
        self.tree.bind("<Double-1>", lambda event: self.onDoubleClick(event))

        # Insert sample data into the Treeview
        for product in self.data:
            self.tree.insert("", "end", values=product)
        self.tree.pack(fill="both", expand=True)

    def onDoubleClick(self, event):
            '''Executed, when a row is double-clicked'''
            # close previous popups
            try:  # in case there was no previous popup
                self.entryPopup.destroy()
            except AttributeError:
                pass

            # what row and column was clicked on
            rowid = self.tree.identify_row(event.y)
            column = self.tree.identify_column(event.x)

            # return if the header was double clicked
            if not rowid:
                return

            # get cell position and cell dimensions
            x, y, width, height = self.tree.bbox(rowid, column)
            print(x, y, width, height)

            # y-axis offset
            pady = height // 2

            # place Entry Widget
            text = self.tree.item(rowid, 'values')[int(column[1:])-1]
            self.entryPopup = EntryPopup(self, rowid, int(column[1:])-1, text)
            self.entryPopup.place(x=x, y=y+pady, width=width, height=height, anchor='w')

    def get_column(self, column_name):
        column_values = []
        for id in self.tree.get_children():
            column_values.append(self.tree.item(id, "values")[self.tree["columns"].index(column_name)])
        return column_values
    
    def get_row(self, row_index):
        print("get_children return value: ", self.tree.get_children())
        return self.tree.item(self.tree.get_children()[row_index], "values")

products = [
        ("1001", "Product A", "Category 1", "$10.99"),
        ("1002", "Product B", "Category 2", "$15.49"),
        ("1003", "Product C", "Category 1", "$8.95"),
        ("1004", "Product D", "Category 3", "$12.99"),
        ("1005", "Product E", "Category 2", "$9.99"),
        ("1006", "Product F", "Category 1", "$7.50"),
        ("1007", "Product G", "Category 3", "$11.25"),
        ("1008", "Product H", "Category 2", "$14.99"),
        ("1009", "Product I", "Category 1", "$6.99"),
        ("1010", "Product J", "Category 3", "$10.00")
    ]

app = Table(products)
app.mainloop()

This marks the end of the How to make ttk.Treeview Editable Tutorial. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the tutorial content can be asked in the comments section below.

Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments