How to make ttk.Treeview's rows editable?
Question:
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.
Answers:
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
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'
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()
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(self, 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(self, 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(self, treeView):
curr = treeView.focus()
if '' == curr: return
treeView.delete(curr)
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
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'
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.
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.
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
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'
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()
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(self, 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(self, 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(self, treeView):
curr = treeView.focus()
if '' == curr: return
treeView.delete(curr)
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
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'
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.