How to make ttk.Treeview's rows editable?
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'
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)
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()