18

Is there any way to use ttk Treeview with editable rows?

I mean it should work more like a table. For example on double click on the item make the #0 column 'editable'.

If this isn't possible, any way to allow mouse selecting on the item would be just fine. I haven't found any mention of this in tkdocs or other documents.

nbro
  • 15,395
  • 32
  • 113
  • 196
dakov
  • 1,039
  • 2
  • 12
  • 33
  • I developed a way to click on a cell in a treeview and create a field on top of the clicked cell so that the cell value could be edited. However, one of the treeview methods that I used to pull this off only works on my Mac, but not on Windows. What's strange is that it's technically not supposed to even work for the Mac, and yet it does. You didn't list your platform, but if you're on a Mac (and won't be running the code on Windows), let me know and I'll post an answer with the details. – Justin S Barrett Sep 15 '13 at 01:18
  • 1
    I've done the same and it works both linux and windows, I don't have a chance to try it on Mac. I don't have to make text editable actually, I've made the Entry widget readonly. So if your 'Mac-only solution' has problem with displaying the Entry popup, maybe my solution could inspire you. See my answer to this question for code example. – dakov Sep 15 '13 at 18:17
  • 1
    I've run into similar limitations, mainly using the Treeview to mimic a table since there is no table-like widget in tkinter/ttk. If you are not using the Treeview as a "tree" you could try tkintertable (https://code.google.com/p/tkintertable/). It essentially allows spreadsheet functionality and is relatively current, well documented and is pretty feature rich. – Fiver Oct 05 '13 at 13:11

8 Answers8

16

After long research I haven't found such feature so I guess there's any. Tk is very simple interface, which allows programmer to build 'high-level' features from the basics. So my desired behaviour this way.

def onDoubleClick(self, event):
    ''' Executed, when a row is double-clicked. Opens 
    read-only EntryPopup above the item's column, so it is possible
    to select text '''

    # close previous popups
    # self.destroyPopups()

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

    # get column position info
    x,y,width,height = self._tree.bbox(rowid, column)

    # y-axis offset
    # pady = height // 2
    pady = 0

    # place Entry popup properly         
    text = self._tree.item(rowid, 'text')
    self.entryPopup = EntryPopup(self._tree, rowid, text)
    self.entryPopup.place( x=0, y=y+pady, anchor=W, relwidth=1)

This is method within a class which composes ttk.Treeview as self._tree

And EntryPopup is then very simple sub-class of Entry:

class EntryPopup(Entry):

    def __init__(self, parent, iid, text, **kw):
        ''' If relwidth is set, then width is ignored '''
        super().__init__(parent, **kw)
        self.tv = parent
        self.iid = iid

        self.insert(0, text) 
        # self['state'] = 'readonly'
        # self['readonlybackground'] = 'white'
        # self['selectbackground'] = '#1BA1E2'
        self['exportselection'] = False

        self.focus_force()
        self.bind("<Return>", self.on_return)
        self.bind("<Control-a>", self.select_all)
        self.bind("<Escape>", lambda *ignore: self.destroy())

    def on_return(self, event):
        self.tv.item(self.iid, text=self.get())
        self.destroy()

    def select_all(self, *ignore):
        ''' Set selection on the whole text '''
        self.selection_range(0, 'end')

        # returns 'break' to interrupt default key-bindings
        return 'break'
stovfl
  • 14,998
  • 7
  • 24
  • 51
dakov
  • 1,039
  • 2
  • 12
  • 33
  • That's very similar to the process I'm using. In my case, I'm using `identify_region` to determine where the user clicked in the treeview, and only display an entry field if the user clicked in a cell. The docs I was initially using said nothing about that method only working with Tk 8.6 and up, so I tried it. Miraculously it worked...on my Mac. It wasn't until it threw an exception on my client's Windows system that I discovered that Python's Tkinter and ttk modules currently use Tk 8.5. Why does it work for me? No idea. At any rate, you gave me some ideas on how to fix it. Thanks! – Justin S Barrett Sep 16 '13 at 23:23
4

You could also pop up a tool window with the editable fields listed with Entries to update the values. This example has a treeview with three columns, and does not use subclasses.

Bind your double click to this:

def OnDoubleClick(treeView):
    # First check if a blank space was selected
    entryIndex = treeView.focus()
    if '' == entryIndex: return

    # Set up window
    win = Toplevel()
    win.title("Edit Entry")
    win.attributes("-toolwindow", True)

    ####
    # Set up the window's other attributes and geometry
    ####

    # Grab the entry's values
    for child in treeView.get_children():
        if child == entryIndex:
            values = treeView.item(child)["values"]
            break

    col1Lbl = Label(win, text = "Value 1: ")
    col1Ent = Entry(win)
    col1Ent.insert(0, values[0]) # Default is column 1's current value
    col1Lbl.grid(row = 0, column = 0)
    col1Ent.grid(row = 0, column = 1)

    col2Lbl = Label(win, text = "Value 2: ")
    col2Ent = Entry(win)
    col2Ent.insert(0, values[1]) # Default is column 2's current value
    col2Lbl.grid(row = 0, column = 2)
    col2Ent.grid(row = 0, column = 3)

    col3Lbl = Label(win, text = "Value 3: ")
    col3Ent = Entry(win)
    col3Ent.insert(0, values[2]) # Default is column 3's current value
    col3Lbl.grid(row = 0, column = 4)
    col3Ent.grid(row = 0, column = 5)

    def UpdateThenDestroy():
        if ConfirmEntry(treeView, col1Ent.get(), col2Ent.get(), col3Ent.get()):
            win.destroy()

    okButt = Button(win, text = "Ok")
    okButt.bind("<Button-1>", lambda e: UpdateThenDestroy())
    okButt.grid(row = 1, column = 4)

    canButt = Button(win, text = "Cancel")
    canButt.bind("<Button-1>", lambda c: win.destroy())
    canButt.grid(row = 1, column = 5)

Then confirm the changes:

def ConfirmEntry(treeView, entry1, entry2, entry3):
    ####
    # Whatever validation you need
    ####

    # Grab the current index in the tree
    currInd = treeView.index(treeView.focus())

    # Remove it from the tree
    DeleteCurrentEntry(treeView)

    # Put it back in with the upated values
    treeView.insert('', currInd, values = (entry1, entry2, entry3))

    return True

Here's how to delete an entry:

def DeleteCurrentEntry(treeView):
    curr = treeView.focus()

    if '' == curr: return

    treeView.delete(curr)
tom
  • 21,844
  • 6
  • 43
  • 36
DCOPTimDowd
  • 231
  • 1
  • 11
4

I have tried @dakov solution but it did not work for me since my treeView has multiple columns and for few more reasons. I made some changes that enhanced it so here is my version

class Tableview(ttk.Treeview):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        tv.bind("<Double-1>", lambda event: self.onDoubleClick(event))

    def onDoubleClick(self, event):
        ''' Executed, when a row is double-clicked. Opens 
        read-only EntryPopup above the item's column, so it is possible
        to select text '''

        # 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.identify_row(event.y)
        column = self.identify_column(event.x)

        # handle exception when header is double click
        if not rowid:
            return

        # get column position info
        x,y,width,height = self.bbox(rowid, column)

        # y-axis offset
        pady = height // 2

        # place Entry popup properly
        text = self.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')

The EntryPopup class

class EntryPopup(ttk.Entry):
    def __init__(self, parent, iid, column, text, **kw):
        ttk.Style().configure('pad.TEntry', padding='1 1 1 1')
        super().__init__(parent, style='pad.TEntry', **kw)
        self.tv = parent
        self.iid = iid
        self.column = column

        self.insert(0, text) 
        # self['state'] = 'readonly'
        # self['readonlybackground'] = 'white'
        # self['selectbackground'] = '#1BA1E2'
        self['exportselection'] = False

        self.focus_force()
        self.select_all()
        self.bind("<Return>", self.on_return)
        self.bind("<Control-a>", self.select_all)
        self.bind("<Escape>", lambda *ignore: self.destroy())


    def on_return(self, event):
        rowid = self.tv.focus()
        vals = self.tv.item(rowid, 'values')
        vals = list(vals)
        vals[self.column] = self.get()
        self.tv.item(rowid, values=vals)
        self.destroy()


    def select_all(self, *ignore):
        ''' Set selection on the whole text '''
        self.selection_range(0, 'end')

        # returns 'break' to interrupt default key-bindings
        return 'break'
xillmera
  • 22
  • 3
Youstanzr
  • 605
  • 1
  • 8
  • 16
1
from tkinter import ttk
from tkinter import *

root = Tk()
columns = ("Items", "Values")
Treeview = ttk.Treeview(root, height=18, show="headings", columns=columns)  # 

Treeview.column("Items", width=200, anchor='center')
Treeview.column("Values", width=200, anchor='center')

Treeview.heading("Items", text="Items")
Treeview.heading("Values", text="Values")

Treeview.pack(side=LEFT, fill=BOTH)

name = ['Item1', 'Item2', 'Item3']
ipcode = ['10', '25', '163']
for i in range(min(len(name), len(ipcode))):
    Treeview.insert('', i, values=(name[i], ipcode[i]))


def treeview_sort_column(tv, col, reverse):
    l = [(tv.set(k, col), k) for k in tv.get_children('')]
    l.sort(reverse=reverse)
    for index, (val, k) in enumerate(l):
        tv.move(k, '', index)
        tv.heading(col, command=lambda: treeview_sort_column(tv, col, not reverse))


def set_cell_value(event):
    for item in Treeview.selection():
        item_text = Treeview.item(item, "values")
        column = Treeview.identify_column(event.x)
        row = Treeview.identify_row(event.y)
    cn = int(str(column).replace('#', ''))
    rn = int(str(row).replace('I', ''))
    entryedit = Text(root, width=10 + (cn - 1) * 16, height=1)
    entryedit.place(x=16 + (cn - 1) * 130, y=6 + rn * 20)

    def saveedit():
        Treeview.set(item, column=column, value=entryedit.get(0.0, "end"))
        entryedit.destroy()
        okb.destroy()

    okb = ttk.Button(root, text='OK', width=4, command=saveedit)
    okb.place(x=90 + (cn - 1) * 242, y=2 + rn * 20)


def newrow():
    name.append('to be named')
    ipcode.append('value')
    Treeview.insert('', len(name) - 1, values=(name[len(name) - 1], ipcode[len(name) - 1]))
    Treeview.update()
    newb.place(x=120, y=(len(name) - 1) * 20 + 45)
    newb.update()


Treeview.bind('<Double-1>', set_cell_value)
newb = ttk.Button(root, text='new item', width=20, command=newrow)
newb.place(x=120, y=(len(name) - 1) * 20 + 45)

for col in columns:
    Treeview.heading(col, text=col, command=lambda _col=col: treeview_sort_column(Treeview, _col, False))


root.mainloop()

After so much research while doing my project got this code, it helped me a lot. Double click on the element you want to edit, make the required change and click 'OK' button I think this is what exactly you wanted

#python #tkinter #treeview #editablerow

New row Editable row

sachin
  • 11
  • 3
0

This is just for creating a tree for the specified path that is set in the constructor. you can bind your event to your item on that tree. The event function is left in a way that the item could be used in many ways. In this case, it will show the name of the item when double clicked on it. Hope this helps somebody.

    import ttk
    from Tkinter import*
    import os*

    class Tree(Frame):

    def __init__(self, parent):
        Frame.__init__(self, parent)
        self.parent = parent
        path = "/home/...."
        self.initUI(path)

    def initUI(self, path):
        self.parent.title("Tree")
        self.tree = ttk.Treeview(self.parent)
        self.tree.bind("<Double-1>", self.itemEvent)
        yScr = ttk.Scrollbar(self.tree, orient = "vertical", command = self.tree.yview)
        xScr = ttk.Scrollbar(self.tree, orient = "horizontal", command = self.tree.xview)
        self.tree.configure(yscroll = yScr.set, xScroll = xScr.set)
        self.tree.heading("#0", text = "My Tree", anchor = 'w')
        yScr.pack(side = RIGHT, fill = Y)

        pathy = os.path.abspath(path) 
        rootNode = self.tree.insert('', 'end', text = pathy, open = True)
        self.createTree(rootNode, pathy)

        self.tree.pack(side = LEFT, fill = BOTH, expand = 1, padx = 2, pady = 2)

        self.pack(fill= BOTH, expand = 1) 

    def createTree(self, parent, path)
        for p in os.listdir(path)
            pathy = os.path.join(path, p)
            isdir = os.path.isdir(pathy)
            oid = self.tree.insert(parent, 'end' text = p, open = False)
            if isdir:
               self.createTree(oid, pathy)

    def itemEvent(self, event):
        item = self.tree.selection()[0] # now you got the item on that tree
        print "you clicked on", self.tree.item(item,"text")



    def main():
        root = Tk.Tk()
        app = Tree(root)
        root.mainloop()

    if __name__ == '__main__'
       main()
Cugomastik
  • 911
  • 13
  • 22
0

You should not do this manually there are ready to use pack that have this Feature and many more such as tkintertable it have some insane features

there is also pygubu-editable-treeview if you are intrested in pygubu,

as for the the reason you shouldnt code your own , in order to do a good treeview you will need to build more Feature that make your gui easier to use however such Feature takes hundred lines of code to create.(takes a long time to get right) unless you are making a custom TREE-View-widget,it doesnot worth the effort.

0

I have tweaked @DCOPTimDowd code to make editing a cell visually more appealing and easy.

Improvements:

  • Can assign non-editable columns
  • Just double click a cell to edit it.
  • More visually appealing.
class PopupEntry(tk.Entry):
    def __init__(self, parent, x, y, textvar,width = 10 ,entry_value='', text_justify = 'left', ):
        super().__init__(parent, relief = 'flat', justify = text_justify,bg='white', textvariable=textvar, font= "sublime 10")
        self.place(x=x, y=y, width=width)
        
        self.textvar = textvar
        self.textvar.set(entry_value)
        self.focus_set()
        self.select_range(0, 'end')
        # move cursor to the end
        self.icursor('end')

        self.wait_var = tk.StringVar(master=self)
        self._bind_widget()

        self.entry_value = entry_value
        self.wait_window()
    
    def _bind_widget(self):
        self.bind("<Return>", self.retrive_value)
        self.bind('<FocusOut>', self.retrive_value)

    def retrive_value(self, e):
        value = self.textvar.get()
        self.destroy()
        self.textvar.set(value)
        
        
class EditableTreeview(ttk.Treeview):
    def __init__(self, parent, columns, show, bind_key,data:list, non_editable_columns = None):
        super().__init__(parent, columns=columns, show=show)
        self.parent = parent
        self.column_name = columns
        self.data = data
        self.bind_key = bind_key
        self.non_editable_columns = non_editable_columns

        self.set_primary_key_column_attributes()
        self.set_headings()
        self.insert_data()
        self.set_edit_bind_key()
    
    def set_primary_key_column_attributes(self):
        self.column("#0",width=100,stretch=1)

    def set_headings(self):
        for i in self.column_name:
            self.heading(column=i, text=i)

    def insert_data(self):
        for values in self.data:
            self.insert('', tk.END, values=values)
    
    def set_edit_bind_key(self):
        self.bind('<Double Button-1>', self.edit)

    def get_absolute_x_cord(self):
        rootx = self.winfo_pointerx()
        widgetx = self.winfo_rootx()

        x = rootx - widgetx

        return x

    def get_absolute_y_cord(self):
        rooty = self.winfo_pointery()
        widgety = self.winfo_rooty()

        y = rooty - widgety

        return y
    
    def get_current_column(self):
        pointer = self.get_absolute_x_cord()
        return self.identify_column(pointer)

    def get_cell_cords(self,row,column):
        return self.bbox(row, column=column)
    
    def get_selected_cell_cords(self):
        row = self.focus()
        column = self.get_current_column()
        return self.get_cell_cords(row = row, column = column)

    def update_row(self, values):
        current_row = self.focus()

        currentindex = self.index(self.focus())

        self.delete(current_row)
        
        # Put it back in with the upated values
        self.insert('', currentindex, values = values)

    def check_region(self):
        result = self.identify_region(x=(self.winfo_pointerx() - self.winfo_rootx()), y=(self.winfo_pointery()  - self.winfo_rooty()))
        print(result)
        if result == 'cell':return True
        else: return False

    def check_non_editable(self):
        if self.get_current_column() in self.non_editable_columns:return False
        else: return True

    def edit(self, e):
        if self.check_region() == False: return
        elif self.check_non_editable() == False: return
        current_row_values = list(self.item(self.focus(),'values'))
        current_column = int(self.get_current_column().replace("#",''))-1
        current_cell_value = current_row_values[current_column]

        entry_cord = self.get_selected_cell_cords()
        entry_x = entry_cord[0]
        entry_y = entry_cord[1]
        entry_w = entry_cord[2]
        entry_h = entry_cord[3]

        entry_var = tk.StringVar()
        
        PopupEntry(self, x=entry_x, y=entry_y, width=entry_w,entry_value=current_cell_value, textvar= entry_var, text_justify='left')

        if entry_var != current_cell_value:
            current_row_values[current_column] = entry_var.get()
            self.update_row(values=current_row_values)

Improvements are always welcomed: github file

Screenshots:

  • Before Double clicking cell. Before Double clicking cell

  • After Double clicking cell. After Double clicking cell
Faraaz Kurawle
  • 1,085
  • 6
  • 24
-1

I don't know about making the row editable, but to capture clicking on a row, you use the <<TreeviewSelect>> virtual event. This gets bound to a routine with the bind() method, then you use the selection() method to get the ids of the items selected.

These are snippets from an existing program, but show the basic sequence of calls:

# in Treeview setup routine
    self.tview.tree.bind("<<TreeviewSelect>>", self.TableItemClick)

# in TableItemClick()
    selitems = self.tview.tree.selection()
    if selitems:
        selitem = selitems[0]
        text = self.tview.tree.item(selitem, "text") # get value in col #0
Todd
  • 5,999
  • 2
  • 21
  • 21