You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I though that you might be interested in something that I have been playing around with for the last day or so. It's basically a class which extends Treeview, so that you can perform in-place data updates, a little like you would in a spreadsheet. It's based on a tutorial which I found online.
Here is the full video, with commentary, showing it in action: TreeEdit.mp4.zip
Here is a much shorter version without sound - not sure why sound doesn't play on Github:
simplescreenrecorder-2023-07-15_17.38.52.mp4
The downloadable version of the video gives a better explanation as to what is going on. Essentially you need to double click an entry to edit it, and hit enter, when you have finished your edit.
The Save button triggers a call-back function which is passed the TreeviewEdit object, and it then calls the TreeviewEdit method to supply a dictionary of the TreeviewEdit data.
Here is the class code:
import tkinter
import tkinter as tk
from tkinter import ttk
import customtkinter as ctk
class TreeviewEdit(ttk.Treeview):
"""Class based on Tkinter Treeview, which allows editing of cells."""
mode = ctk.get_appearance_mode()
if mode == 'Light':
mode_idx = 0
else:
mode_idx = 1
odd_color = ctk.ThemeManager.theme["CTkButton"]["fg_color"][mode_idx]
even_color = ctk.ThemeManager.theme["CTkButton"]["hover_color"][mode_idx]
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.bind('<Double-1>', self.on_double_click)
def on_double_click(self, event: tkinter.Event) -> None:
"""The on_double_click method, places us in cell edit mode."""
region_clicked = self.identify_region(event.x, event.y)
if region_clicked not in ('tree', 'cell'):
return
# Which item was double clicked...
column = self.identify_column(event.x)
# Here we get the column index for the values - these are offset by 1, becuase the
# tree entry (1st column) takes up 0. So we need to subtract 1.
column_index = int(column[1:]) - 1
selected_iid = self.focus()
selected_values = self.item(selected_iid)
if column == '#0':
selected_text = selected_values.get('text')
else:
try:
selected_text = selected_values.get('values')[column_index]
except IndexError:
# We have clicked location without data
return
column_box = self.bbox(selected_iid, column)
entry_edit = ttk.Entry(root, width=column_box[2])
# Record the column index and item iid
entry_edit.editing_column_index = column_index
entry_edit.editing_item_id = selected_iid
entry_edit.insert(0, selected_text)
entry_edit.select_range(0, tk.END)
entry_edit.place(x=column_box[0],
y=column_box[1],
w=column_box[2],
h=column_box[3])
entry_edit.focus()
entry_edit.bind('<FocusOut>', self.on_focus_out)
entry_edit.bind('<Return>', self.on_enter_pressed)
def on_focus_out(self, event: tkinter.Event) -> None:
"""The on_focus_out method, destroys our edit widget. Any changes are not persisted."""
event.widget.destroy()
def on_enter_pressed(self, event: tkinter.Event) -> None:
"""The on_enter_pressed is actioned when an entry has been put in edit mode and the Enter key is pressed.
Any changes are persisted to the cell."""
new_text = event.widget.get()
selected_iid = event.widget.editing_item_id
column_index = event.widget.editing_column_index
if column_index == -1:
self.item(selected_iid, text=new_text)
else:
current_values = self.item(selected_iid).get('values')
current_values[column_index] = new_text
self.item(selected_iid, values=current_values)
event.widget.destroy()
def expand_all_nodes(self):
"""
Expands all node entries
"""
children = self.get_children()
for child in children:
self.expand_node(child)
def contract_all_nodes(self):
"""
Contract all node entries
"""
children = self.tree.get_children()
for child in children:
self.contract_node(child)
def apply_stripes(self, odd_color=None, even_color=None):
if odd_color is None:
_odd_color = TreeviewEdit.odd_color
else:
# Update the class colour in case the appy_stripes is called subsequently,
# without colour parameters
TreeviewEdit.odd_color = odd_color
_odd_color = odd_color
if even_color is None:
_even_color = TreeviewEdit.even_color
else:
# Update the class colour in case the appy_stripes is called subsequently,
# without colour parameters
TreeviewEdit.odd_color = even_color
_even_color = even_color
col = TreeviewEdit.odd_color
self.tag_configure('odd_row', background=_odd_color)
self.tag_configure('even_row', background=_even_color)
for index, iid in enumerate(self.get_children()):
tags = self.item(iid, 'tags') # get current tags
tags.remove('odd_row') if 'odd_row' in tags else None # remove if exist
tags.remove('even_row') if 'even_row' in tags else None # remove if exist
self.item(iid, tags=tags)
if index % 2 == 0:
self.item(iid, tags='even_row')
else:
self.item(iid, tags='odd_row')
def expand_node(self, node: str) -> None:
"""
Expand the passed node and its dependencies.
:param node: TreeEdit/TreeView root node
"""
self.item(node, open=True)
children = self.get_children(node)
if len(children) > 0:
for child in children:
self.expand_node(child)
def contract_node(self, node: str) -> None:
"""
Contract the passed node and its dependencies.
:param node: TreeEdit/TreeView root node
"""
children = self.get_children(node)
if len(children) > 0:
for child in children:
self.contract_node(child)
self.item(node, open=False)
def dump_as_dictionary(self) -> dict:
values = []
dump_dict = {}
# Here parent is the iid
for parent in self.get_children():
col0_text = self.item(parent)["text"]
dump_dict[col0_text] = []
for child in self.get_children(parent):
dump_dict[col0_text].append(self.item(child)["values"])
return dump_dict
def dump_tree_dict(tree_edit_widget: TreeviewEdit):
"""Recieves a TreeviewEdit object and creates a dictionary of its contents."""
print('=' * 50)
tree_dict = tree_edit_widget.dump_as_dictionary()
for key, records in tree_dict.items():
for record in records:
print(f'{key}: {record[0]} / {record[1]} / {record[2]}')
print('=' * 50 + '\n')
return tree_dict
Here is the full demo code. It determines the CustomTkinter theme, and chooses colours to blend:
import tkinter
import tkinter as tk
from tkinter import ttk
import customtkinter as ctk
class TreeviewEdit(ttk.Treeview):
"""Class based on Tkinter Treeview, which allows editing of cells."""
mode = ctk.get_appearance_mode()
if mode == 'Light':
mode_idx = 0
else:
mode_idx = 1
odd_color = ctk.ThemeManager.theme["CTkButton"]["fg_color"][mode_idx]
even_color = ctk.ThemeManager.theme["CTkButton"]["hover_color"][mode_idx]
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.bind('<Double-1>', self.on_double_click)
def on_double_click(self, event: tkinter.Event) -> None:
"""The on_double_click method, places us in cell edit mode."""
region_clicked = self.identify_region(event.x, event.y)
if region_clicked not in ('tree', 'cell'):
return
# Which item was double clicked...
column = self.identify_column(event.x)
# Here we get the column index for the values - these are offset by 1, becuase the
# tree entry (1st column) takes up 0. So we need to subtract 1.
column_index = int(column[1:]) - 1
selected_iid = self.focus()
selected_values = self.item(selected_iid)
if column == '#0':
selected_text = selected_values.get('text')
else:
try:
selected_text = selected_values.get('values')[column_index]
except IndexError:
# We have clicked location without data
return
column_box = self.bbox(selected_iid, column)
entry_edit = ttk.Entry(root, width=column_box[2])
# Record the column index and item iid
entry_edit.editing_column_index = column_index
entry_edit.editing_item_id = selected_iid
entry_edit.insert(0, selected_text)
entry_edit.select_range(0, tk.END)
entry_edit.place(x=column_box[0],
y=column_box[1],
w=column_box[2],
h=column_box[3])
entry_edit.focus()
entry_edit.bind('<FocusOut>', self.on_focus_out)
entry_edit.bind('<Return>', self.on_enter_pressed)
def on_focus_out(self, event: tkinter.Event) -> None:
"""The on_focus_out method, destroys our edit widget. Any changes are not persisted."""
event.widget.destroy()
def on_enter_pressed(self, event: tkinter.Event) -> None:
"""The on_enter_pressed is actioned when an entry has been put in edit mode and the Enter key is pressed.
Any changes are persisted to the cell."""
new_text = event.widget.get()
selected_iid = event.widget.editing_item_id
column_index = event.widget.editing_column_index
if column_index == -1:
self.item(selected_iid, text=new_text)
else:
current_values = self.item(selected_iid).get('values')
current_values[column_index] = new_text
self.item(selected_iid, values=current_values)
event.widget.destroy()
def expand_all_nodes(self):
"""
Expands all node entries
"""
children = self.get_children()
for child in children:
self.expand_node(child)
def contract_all_nodes(self):
"""
Contract all node entries
"""
children = self.tree.get_children()
for child in children:
self.contract_node(child)
def apply_stripes(self, odd_color=None, even_color=None):
if odd_color is None:
_odd_color = TreeviewEdit.odd_color
else:
# Update the class colour in case the appy_stripes is called subsequently,
# without colour parameters
TreeviewEdit.odd_color = odd_color
_odd_color = odd_color
if even_color is None:
_even_color = TreeviewEdit.even_color
else:
# Update the class colour in case the appy_stripes is called subsequently,
# without colour parameters
TreeviewEdit.odd_color = even_color
_even_color = even_color
col = TreeviewEdit.odd_color
self.tag_configure('odd_row', background=_odd_color)
self.tag_configure('even_row', background=_even_color)
for index, iid in enumerate(self.get_children()):
tags = self.item(iid, 'tags') # get current tags
tags.remove('odd_row') if 'odd_row' in tags else None # remove if exist
tags.remove('even_row') if 'even_row' in tags else None # remove if exist
self.item(iid, tags=tags)
if index % 2 == 0:
self.item(iid, tags='even_row')
else:
self.item(iid, tags='odd_row')
def expand_node(self, node: str) -> None:
"""
Expand the passed node and its dependencies.
:param node: TreeEdit/TreeView root node
"""
self.item(node, open=True)
children = self.get_children(node)
if len(children) > 0:
for child in children:
self.expand_node(child)
def contract_node(self, node: str) -> None:
"""
Contract the passed node and its dependencies.
:param node: TreeEdit/TreeView root node
"""
children = self.get_children(node)
if len(children) > 0:
for child in children:
self.contract_node(child)
self.item(node, open=False)
def dump_as_dictionary(self) -> dict:
values = []
dump_dict = {}
# Here parent is the iid
for parent in self.get_children():
col0_text = self.item(parent)["text"]
dump_dict[col0_text] = []
for child in self.get_children(parent):
dump_dict[col0_text].append(self.item(child)["values"])
return dump_dict
def dump_tree_dict(tree_edit_widget: TreeviewEdit):
"""Recieves a TreeviewEdit object and creates a dictionary of its contents."""
print('=' * 50)
tree_dict = tree_edit_widget.dump_as_dictionary()
for key, records in tree_dict.items():
for record in records:
print(f'{key}: {record[0]} / {record[1]} / {record[2]}')
print('=' * 50 + '\n')
return tree_dict
if __name__ == '__main__':
root = ctk.CTk()
ctk.set_default_color_theme('blue')
column_names = ('vehicle_name', 'year', 'colour')
odd_color = ctk.ThemeManager.theme["DropdownMenu"]["fg_color"]
even_color = ctk.ThemeManager.theme["DropdownMenu"]["fg_color"]
top_fg_color = root._apply_appearance_mode(ctk.ThemeManager.theme["CTkFrame"]["top_fg_color"])
bg_color = root._apply_appearance_mode(ctk.ThemeManager.theme["CTkFrame"]["fg_color"])
top_fg_color = root._apply_appearance_mode(ctk.ThemeManager.theme["CTkFrame"]["top_fg_color"])
text_color = root._apply_appearance_mode(ctk.ThemeManager.theme["CTkLabel"]["text_color"])
selected_color = root._apply_appearance_mode(ctk.ThemeManager.theme["CTkButton"]["fg_color"])
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
treestyle = ttk.Style()
treestyle.theme_use('default')
treestyle.configure("Treeview", background=bg_color, foreground=text_color, fieldbackground=top_fg_color,
borderwidth=0)
treestyle.map('Treeview', background=[('selected', bg_color)], foreground=[('selected', text_color)])
frm_main = ctk.CTkFrame(master=root)
frm_main.grid(row=0, column=0, sticky='nsew')
frm_tree = ctk.CTkFrame(master=frm_main)
frm_tree.grid(row=0, column=0, sticky='nsew')
frm_main.rowconfigure(0, weight=1)
frm_main.columnconfigure(0, weight=1)
frm_buttons = ctk.CTkFrame(master=root, corner_radius=0)
frm_buttons.grid(row=1, column=0, pady=2, sticky='nsew')
frm_tree.rowconfigure(0, weight=1)
frm_tree.columnconfigure(0, weight=1)
# treeview_vehicles = TreeviewEdit(root, columns=column_names, show='headings')
treeview_vehicles = TreeviewEdit(frm_tree, columns=column_names)
treeview_vehicles.grid(row=0, column=0, sticky='nsew')
treeview_vehicles.heading('#0', text='Vehicle Type')
treeview_vehicles.heading('vehicle_name', text='Vehicle Model')
treeview_vehicles.heading('year', text='Year')
treeview_vehicles.heading('colour', text='Colour')
btn_cancel = ctk.CTkButton(master=frm_buttons,text='Close', command=root.destroy)
btn_cancel.grid(row=0, column=0, padx=10, pady=5)
btn_save = ctk.CTkButton(master=frm_buttons,text='Save', command=lambda tree_edit=treeview_vehicles:
dump_tree_dict(tree_edit_widget=tree_edit))
btn_save.grid(row=0, column=1, padx=10, pady=5)
saloon_row = treeview_vehicles.insert(parent='',
index=tk.END,
text='Saloon'
)
treeview_vehicles.insert(parent=saloon_row,
index=tk.END,
value=('Nissan Versa', '2010', 'Silver')
)
treeview_vehicles.insert(parent=saloon_row,
index=tk.END,
value=('Toyota Corolla', '2012', 'Blue')
)
treeview_vehicles.insert(parent=saloon_row,
index=tk.END,
value=('Ford Capri', '1983', 'Martian Red')
)
people_carrier_row = treeview_vehicles.insert(parent='',
index=tk.END,
text='People Carrier'
)
treeview_vehicles.insert(parent=people_carrier_row,
index=tk.END,
value=('Nissan Qashqai', '2015', 'Ruby Red')
)
treeview_vehicles.insert(parent=people_carrier_row,
index=tk.END,
value=('Landrover Defender', '2015', 'Black')
)
treeview_vehicles.insert(parent=people_carrier_row,
index=tk.END,
value=('Fiat Kubo', '2009', 'White')
)
sports_row = treeview_vehicles.insert(parent='',
index=tk.END,
text='Sports Car'
)
treeview_vehicles.insert(parent=sports_row,
index=tk.END,
value=('Mazda MX-5', '2016', 'Red')
)
treeview_vehicles.insert(parent=sports_row,
index=tk.END,
value=('BMW Z4', '2018', 'Black')
)
treeview_vehicles.insert(parent=sports_row,
index=tk.END,
value=('Audi R8', '2017', 'Black')
)
treeview_vehicles.insert(parent=sports_row,
index=tk.END,
value=('Audi R8', '2017', 'Black')
)
# Get the widget to expand all the nodes to make all record immediately visible.
treeview_vehicles.expand_all_nodes()
# Here we grab some CustomTkinter specifics so that we can colour the
# widget components as to blend with the theme.
mode = ctk.get_appearance_mode()
if mode == 'Light':
mode_idx = 0
else:
mode_idx = 1
odd_color = ctk.ThemeManager.theme["CTkButton"]["fg_color"][mode_idx]
even_color = ctk.ThemeManager.theme["CTkButton"]["hover_color"][mode_idx]
treeview_vehicles.apply_stripes(odd_color=odd_color, even_color=even_color)
# Print the initial view of the data.
print(f'INITIAL DATA:')
dump_as_dictionary = dump_tree_dict(treeview_vehicles)
root.mainloop()
Well I realised afterwards, that the Yaris is a model made by Toyota, but at least you get the idea :o)
Hope you find this useful. It would be great if someone could take this and give it some real polish.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
Hello ladies/gents.
I though that you might be interested in something that I have been playing around with for the last day or so. It's basically a class which extends Treeview, so that you can perform in-place data updates, a little like you would in a spreadsheet. It's based on a tutorial which I found online.
Here is the full video, with commentary, showing it in action:
TreeEdit.mp4.zip
Here is a much shorter version without sound - not sure why sound doesn't play on Github:
simplescreenrecorder-2023-07-15_17.38.52.mp4
The downloadable version of the video gives a better explanation as to what is going on. Essentially you need to double click an entry to edit it, and hit enter, when you have finished your edit.
The Save button triggers a call-back function which is passed the TreeviewEdit object, and it then calls the TreeviewEdit method to supply a dictionary of the TreeviewEdit data.
Here is the class code:
Here is the full demo code. It determines the CustomTkinter theme, and chooses colours to blend:
Well I realised afterwards, that the Yaris is a model made by Toyota, but at least you get the idea :o)
Hope you find this useful. It would be great if someone could take this and give it some real polish.
Beta Was this translation helpful? Give feedback.
All reactions