3
\$\begingroup\$

I have subclassed tkinter treeview object added many features like cut,paste etc and context menus to insert rows, delete etc. Would like some feedback on how well it works and if it's intuitive or not.

Current version is @: https://github.com/unodan/TkInter-Treeview-Example-Demo

import json
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkfont
from os import path, makedirs
from sys import platform
from datetime import datetime
ABS_PATH = path.dirname(path.realpath(__file__))
_IID = 0
_TYPE = 1
_OPEN = 2
_TAGS = 3
_SIZE = 4
_MODIFIED = 5
_DATA1 = 6
_SKIP = 0
_CANCEL = 1
class App(tk.Tk):
 def __init__(self):
 super().__init__()
 self.rowconfigure(0, weight=1)
 self.columnconfigure(0, weight=1)
 self.frame = ttk.Frame(self)
 self.frame.rowconfigure(0, weight=1)
 self.frame.columnconfigure(0, weight=1)
 self.frame.grid(sticky=tk.NSEW)
 self.app_data = {}
 self.style = ttk.Style()
 self.style.theme_use('clam')
 self.title('Treeview Demo')
 self.protocol('WM_DELETE_WINDOW', self.exit)
 self.platform = platform
 if platform == "linux" or platform == "linux2":
 self.platform = 'linux'
 self.setup()
 def setup(self):
 def setup_app():
 file = path.join(ABS_PATH, 'app.json')
 if path.exists(file):
 with open(file) as f:
 self.app_data = json.load(f)
 else:
 self.app_data = {
 'geometry': '500x700',
 }
 def setup_treeview():
 tv_line_padding = 8
 tv_heading_padding = 5
 tv_heading_border_width = 2
 font = tkfont.nametofont('TkDefaultFont')
 self.linespace = font.metrics('linespace')
 row_height = self.linespace + tv_line_padding
 tv_indent = row_height
 self.style.configure('Treeview', rowheight=row_height)
 self.style.configure('Treeview.Heading', padding=tv_heading_padding, borderwidth=tv_heading_border_width)
 self.style.configure('Treeview', indent=tv_indent)
 self.style.configure('TEntry', selectbackground='#0081c1')
 self.style.map('Treeview', background=[('selected', '#0081c1')])
 self.style.configure('TCombobox', selectbackground='#0081c1')
 self.style.map(
 'TCombobox',
 foreground=[('readonly', 'white')],
 fieldbackground=[('readonly', '#0081c1')],
 )
 self.option_add("*TCombobox*Listbox*Background", 'white')
 self.option_add("*TCombobox*Listbox*Foreground", '#000000')
 file = path.join(ABS_PATH, 'treeview.json')
 if path.exists(file):
 with open(file) as f:
 setup = json.load(f)
 else:
 now = datetime.now()
 dt_string = now.strftime("%Y/%m/%d %H:%M:%S")
 setup = {
 'headings': (
 {'text': 'Name', 'anchor': tk.W},
 {'text': 'IID', 'anchor': tk.W},
 {'text': 'Item', 'anchor': tk.W},
 {'text': 'Open', 'anchor': tk.W},
 {'text': 'Tags', 'anchor': tk.W},
 {'text': 'Size', 'anchor': tk.W},
 {'text': 'Last Modified', 'anchor': tk.W},
 {'text': 'Data', 'anchor': tk.W},
 ),
 'columns': (
 {'width': 180, 'minwidth': 3, 'stretch': tk.NO, 'type': 'Entry', 'unique': True},
 {'width': 70, 'minwidth': 3, 'stretch': tk.NO},
 {'width': 70, 'minwidth': 3, 'stretch': tk.NO},
 {'width': 70, 'minwidth': 3, 'stretch': tk.NO},
 {'width': 120, 'minwidth': 3, 'stretch': tk.NO},
 {'width': 80, 'minwidth': 3, 'stretch': tk.NO},
 {'width': 130, 'minwidth': 3, 'stretch': tk.NO},
 {'width': 180, 'minwidth': 3, 'stretch': tk.YES, 'type': 'Combobox',
 'values': ('Value 1', 'Value 2', 'Value 3', 'Value 4', 'Value 5'),
 },
 ),
 'data': (
 {'text': 'Folder 0', 'open': 1, 'values': ('', 'Node', True, '', '', dt_string, ''),
 'children': (
 {'text': 'photo1.png', 'values': ('', 'Leaf', '', '', '0 Kb', dt_string, '')},
 {'text': 'photo2.png', 'values': ('', 'Leaf', '', '', '0 Kb', dt_string, '')},
 {'text': 'photo3.png', 'values': ('', 'Leaf', '', '', '0 Kb', dt_string, '')},
 {'text': 'Folder 0_1', 'open': 1, 'values': ('', 'Node', True, '', '', dt_string, ''),
 'children': (
 {'text': 'photo1.png', 'values': ('', 'Leaf', '', '', '0 Kb', dt_string, '')},
 {'text': 'photo2.png', 'values': ('', 'Leaf', '', '', '0 Kb', dt_string, '')},
 {'text': 'photo3.png', 'values': ('', 'Leaf', '', '', '0 Kb', dt_string, '')},
 )},
 )},
 {'text': 'Folder 1', 'open': 1, 'values': ('', 'Node', True, '', '', dt_string, ''),
 'children': (
 {'text': 'photo4.png', 'values': ('', 'Leaf', '', '', '0 Kb', dt_string, '')},
 {'text': 'photo5.png', 'values': ('', 'Leaf', '', '', '0 Kb', dt_string, '')},
 {'text': 'photo6.png', 'values': ('', 'Leaf', '', '', '0 Kb', dt_string, '')},
 )},
 ),
 }
 tree = self.treeview = Treeview(self.frame, setup=setup)
 tree.focus_set()
 settings = dict(setup.get('settings', ()))
 item = settings.get('focus', None)
 if (not item or not tree.exists(item)) and tree.get_children():
 item = tree.get_children()[0]
 view = settings.get('view', None)
 if view:
 self.treeview.xview('moveto', view[0])
 self.treeview.yview('moveto', view[1])
 tree.focus(item)
 tree.selection_add(item)
 tree.grid(sticky=tk.NSEW, row=0, column=0)
 setup_app()
 setup_treeview()
 self.geometry(self.app_data['geometry'])
 def exit(self):
 self.app_data.update({'geometry': self.geometry()})
 self.save()
 self.destroy()
 def save(self):
 file = path.join(ABS_PATH, 'app.json')
 if file:
 dirname = path.dirname(file)
 if not path.exists(dirname):
 makedirs(dirname)
 with open(file, 'w') as f:
 json.dump(self.app_data, f, indent=3)
 file = path.join(ABS_PATH, 'treeview.json')
 if file:
 dirname = path.dirname(file)
 if not path.exists(dirname):
 makedirs(dirname)
 with open(file, 'w') as f:
 data = self.treeview.serialize()
 data['settings'] = tuple({
 'view': (self.treeview.xview()[0], self.treeview.yview()[0]),
 'focus': self.treeview.focus()
 }.items())
 for idx, c in enumerate(self.treeview.columns):
 c['width'] = self.treeview.column(f'#{idx}', 'width')
 json.dump(data, f, indent=3)
class Event:
 def __init__(self):
 super().__init__()
 self.x, self.y = None, None
class DialogBase(tk.Toplevel):
 def __init__(self, parent, **kwargs):
 width = kwargs.pop('width', None)
 height = kwargs.pop('height', None)
 title = kwargs.pop('title', '')
 resizable = kwargs.pop('resizable', (True, True))
 super().__init__(parent, **kwargs)
 self.resizable(*resizable)
 self.rowconfigure(0, weight=1)
 self.columnconfigure(0, weight=1)
 self.results = None
 self.container = ttk.Frame(self)
 self.container.grid(sticky=tk.NSEW)
 self.options = kwargs
 geometry = self.geometry().split('+', 1)
 _width, _height = geometry[0].split('x')
 if width:
 _width = width
 if height:
 _height = height
 self.title(title)
 self.geometry(f'{_width}x{_height}+{geometry[1]}')
class RenameDialog(DialogBase):
 def __init__(self, parent, **kwargs):
 message = kwargs.pop('message', 'No Message!')
 super().__init__(parent, **kwargs)
 self.container.rowconfigure(1, weight=1)
 self.container.columnconfigure(0, weight=1)
 frame = self.row0 = ttk.Frame(self.container)
 frame.rowconfigure(0, weight=1)
 frame.columnconfigure(0, weight=1)
 self.label = ttk.Label(frame, text=message)
 self.label.grid(sticky=tk.N+tk.EW, pady=(0, 10), row=0, column=0)
 self.entry = Entry(frame, width=30)
 self.entry.config(textvariable=self.entry.var)
 self.entry.grid(sticky=tk.NSEW, row=1, column=0, padx=(5, 0))
 frame.grid(row=0, sticky=tk.EW, padx=10, pady=(20, 0))
 frame = self.row1 = ttk.Frame(self.container)
 frame.rowconfigure(0, weight=1)
 frame.columnconfigure(0, weight=1)
 self.button_rename = ttk.Button(frame, text="Rename", width=8)
 self.button_rename.grid(sticky=tk.NS + tk.E, row=0, column=0, padx=(5, 0))
 self.button_skip = ttk.Button(frame, text="Skip", width=8)
 self.button_skip.grid(sticky=tk.NS + tk.E, row=0, column=1, padx=(5, 0))
 self.button_cancel = ttk.Button(frame, text="Cancel", width=8)
 self.button_cancel.grid(sticky=tk.NS+tk.E, row=0, column=2, padx=(5, 0))
 frame.grid(row=1, sticky=tk.EW+tk.S, padx=10, pady=(10, 20))
class MessageDialog(DialogBase):
 def __init__(self, parent, **kwargs):
 message = kwargs.pop('message', '')
 super().__init__(parent, **kwargs)
 frame = self.row0 = ttk.Frame(self.container)
 frame.rowconfigure(0, weight=1)
 frame.columnconfigure(0, weight=1)
 self.label = ttk.Label(frame, text=message)
 self.label.grid(sticky=tk.NSEW, pady=5, row=0, column=0)
 frame.grid(sticky=tk.EW, padx=10, pady=(20, 0))
 frame = self.row1 = ttk.Frame(self.container)
 frame.rowconfigure(0, weight=1)
 frame.columnconfigure(0, weight=1)
 self.button_ok = ttk.Button(frame, text="Okay")
 self.button_ok.grid(sticky=tk.NS + tk.E, row=0, column=0, padx=(5, 0))
 frame.grid(row=1, sticky=tk.EW, padx=10, pady=(10, 20))
class Text(tk.Text):
 def __init__(self, parent, **kwargs):
 super().__init__(parent, **kwargs)
 self.origin_x = self.origin_y = 0
class Frame(ttk.Frame):
 def __init__(self, parent, **kwargs):
 super().__init__(parent, **kwargs)
 self.scroll_x = \
 self.scroll_y = None
class Entry(ttk.Entry):
 def __init__(self, parent, **kwargs):
 super().__init__(parent, **kwargs)
 self.var = tk.StringVar()
 self.configure(textvariable=self.var)
 self.undo_data = {}
 self.popup = \
 self.menu_background = None
 self.style = ttk.Style()
 self.setup()
 self.bindings_set()
 def setup(self):
 def set_popup_menu():
 opts = dict(self.style.map('Treeview', 'background'))
 background = self.style.lookup('Treeview.Heading', 'background')
 popup = self.popup = tk.Menu(
 self.winfo_toplevel(),
 tearoff=0,
 background=background,
 foreground='#000000',
 activebackground=opts['selected']
 )
 popup.add_command(label="Select All", command=self.select_all)
 popup.add_separator()
 popup.add_command(label="Cut", command=lambda: self.event_generate('<Control-x>'))
 popup.add_command(label="Copy", command=lambda: self.event_generate('<Control-c>'))
 popup.add_command(label="Paste", command=lambda: self.event_generate('<Control-v>'))
 popup.add_separator()
 popup.add_command(label="Delete", command=self.clear)
 self.menu_background = self.style.lookup('TScrollbar.thumb', 'background')
 set_popup_menu()
 def clear(self):
 self.delete(0, tk.END)
 def select_all(self):
 self.select_range(0, tk.END)
 self.icursor(tk.END)
 def popup_menu(self, event):
 if not self.popup:
 return
 wdg = event.widget
 wdg.focus_set()
 self.popup.tk_popup(event.x_root, event.y_root)
 def bindings_set(self):
 bindings = {
 '<ButtonPress-3>': self.popup_menu,
 }
 for command, callback in bindings.items():
 self.bind(command, callback)
class Combobox(ttk.Combobox):
 def __init__(self, parent, **kwargs):
 super().__init__(parent, **kwargs)
 self.var = tk.StringVar()
 self.configure(textvariable=self.var)
 self.popup = \
 self.menu_background = None
 self.style = ttk.Style()
 self.setup()
 self.bindings_set()
 def setup(self):
 def set_popup_menu():
 opts = dict(self.style.map('Treeview', 'background'))
 background = self.style.lookup('Treeview.Heading', 'background')
 popup = self.popup = tk.Menu(
 self.winfo_toplevel(),
 tearoff=0,
 background=background,
 foreground='#000000',
 activebackground=opts['selected']
 )
 popup.add_command(label="Select All", command=self.select_all)
 popup.add_separator()
 popup.add_command(label="Cut", command=lambda: self.event_generate('<Control-x>'))
 popup.add_command(label="Copy", command=lambda: self.event_generate('<Control-c>'))
 popup.add_command(label="Paste", command=lambda: self.event_generate('<Control-v>'))
 popup.add_separator()
 popup.add_command(label="Delete", command=self.clear)
 self.menu_background = self.style.lookup('TScrollbar.thumb', 'background')
 set_popup_menu()
 def select_all(self):
 self.select_range(0, tk.END)
 self.icursor(tk.END)
 def clear(self):
 self.var.set('')
 def popup_menu(self, event):
 if not self.popup:
 return
 wdg = event.widget
 wdg.focus_set()
 self.popup.tk_popup(event.x_root, event.y_root)
 def bindings_set(self):
 bindings = {
 '<ButtonPress-3>': self.popup_menu,
 }
 for command, callback in bindings.items():
 self.bind(command, callback)
class Label(ttk.Label):
 def __init__(self, parent, **kwargs):
 self.var = tk.StringVar()
 super().__init__(parent, textvariable=self.var, **kwargs)
 self.var.set(kwargs.get('text', ''))
class Listbox(tk.Listbox):
 def __init__(self, parent, **kwargs):
 super().__init__(parent, **kwargs)
 self.var = tk.StringVar()
 self.config(listvariable=self.var)
 def set_row_colors(self, odd, even):
 for i in range(0, len(self.get(0, tk.END))):
 color = odd if i % 2 else even
 self.itemconfig(i, {'bg': color})
class Scrollbar(ttk.Scrollbar):
 def __init__(self, parent, **kwargs):
 self.callback = kwargs.pop('callback', None)
 super().__init__(parent, **kwargs)
 def set(self, low, high):
 if float(low) > 0 or float(high) < 1:
 ttk.Scrollbar.set(self, low, high)
 self.grid()
 else:
 self.grid_remove()
 if self.callback:
 self.callback(self)
class Treeview(ttk.Treeview):
 def __init__(self, parent, **kwargs):
 self.frame = Frame(parent)
 self.frame.rowconfigure(0, weight=1)
 self.frame.columnconfigure(0, weight=1)
 setup = kwargs.pop('setup', {})
 data = setup.pop('data', [])
 self.columns = setup['columns']
 self.headings = setup['headings']
 self.scroll = kwargs.pop('scroll', (True, True))
 super().__init__(self.frame, **kwargs)
 self.detached = []
 self.undo_data = {}
 self.sorted_columns = {}
 self.shift = \
 self.popup = \
 self.scroll_x = \
 self.scroll_y = \
 self.selected = \
 self.dlg_results = \
 self.active_popup_widget = \
 self.active_popup_column = \
 self.cursor_offset = \
 self.menu_background = None
 self.style = ttk.Style()
 self.indent = self.style.lookup('Treeview', 'indent')
 self.rowheight = self.style.lookup('Treeview', 'rowheight')
 if setup:
 self.setup(setup)
 if data:
 self.populate('', data)
 self.bindings_set()
 self.frame.grid(sticky=tk.NSEW)
 def setup(self, data):
 def set_style():
 background = self.style.lookup("TFrame", "background")
 self.tag_configure('odd', background=background)
 self.tag_configure('even', background='#ffffff')
 self.troughcolor = self.style.lookup('TScrollbar.trough', 'troughcolor')
 self.menu_background = self.style.lookup('TScrollbar.Heading', 'background')
 self.style.configure(".", indicatorsize=self.rowheight / 2 + 1)
 def set_popup_menu():
 opts = dict(self.style.map('Treeview', 'background'))
 background = self.style.lookup('Treeview.Heading', 'background')
 popup = self.popup = tk.Menu(
 self.winfo_toplevel(),
 tearoff=0,
 background=background,
 foreground='#000000',
 activebackground=opts['selected']
 )
 create_new = tk.Menu(
 popup,
 tearoff=0,
 background=background,
 foreground='#000000',
 activebackground=opts['selected']
 )
 popup.add_cascade(label="Insert", menu=create_new)
 popup.add_separator()
 popup.add_command(label="Cut", command=self.cut)
 popup.add_command(label="Copy", command=self.copy)
 popup.add_command(label="Paste", command=self.paste)
 popup.add_separator()
 popup.add_command(label="Delete", command=self.detach)
 create_new.add_command(label="Item", command=self.insert_leaf)
 create_new.add_separator()
 create_new.add_command(label="Folder", command=self.insert_node)
 def set_scrollbars():
 scroll_x, scroll_y = self.scroll
 if scroll_x:
 sb_x = self.scroll_x = Scrollbar(self.frame, callback=popup_widget_destroy)
 sb_x.configure(command=self.xview, orient=tk.HORIZONTAL)
 sb_x.grid(sticky=tk.NSEW, row=980, column=0)
 self.configure(xscrollcommand=sb_x.set)
 if scroll_y:
 sb_y = self.scroll_y = Scrollbar(self.frame, callback=popup_widget_destroy)
 sb_y.configure(command=self.yview)
 self.configure(yscrollcommand=sb_y.set)
 sb_y.grid(sticky=tk.NSEW, row=0, column=990)
 def set_rows_columns():
 ids = []
 columns = len(data['columns'])
 for column in range(1, columns):
 ids.append(f'#{column}')
 self["columns"] = ids
 for idx, cfg in enumerate(data['headings']):
 _id = cfg['column'] if 'column' in cfg else f'#{idx}'
 self.heading(_id, text=cfg['text'], anchor=cfg['anchor'])
 self.sorted_columns[f'#{idx}'] = True
 for idx, cfg in enumerate(data['columns']):
 _id = cfg['column'] if 'column' in cfg else f'#{idx}'
 self.column(_id, width=cfg['width'], minwidth=cfg['minwidth'], stretch=cfg['stretch'])
 def popup_widget_destroy(_):
 if self.active_popup_widget:
 self.active_popup_widget.destroy()
 self.active_popup_widget = None
 set_style()
 set_popup_menu()
 set_scrollbars()
 set_rows_columns()
 self.after(1, self.tags_reset)
 def next(self, item):
 if self.item(item, 'open') and self.get_children(item):
 _next = self.get_children(item)[0]
 return _next
 _next = super(Treeview, self).next(item)
 if not _next and self.next(self.parent(item)):
 _next = self.next(self.parent(item))
 return _next
 def prev(self, item):
 _prev = super(Treeview, self).prev(item)
 if not _prev:
 parent = self.parent(item)
 _prev = parent if parent else ''
 return _prev
 def tag_add(self, tags, item):
 self.tags_update('add', tags, item)
 def tag_remove(self, tags, item=None):
 self.tags_update('remove', tags, item)
 def tags_reset(self, excluded=None):
 def reset(_item):
 tags = list(self.item(_item, 'tags'))
 for _tag in tags.copy():
 if _tag not in exclude:
 tags.pop(tags.index(_tag))
 self.item(_item, tags=tags)
 for node in self.get_children(_item):
 reset(node)
 def set_tag(_item, _tag):
 _tag = 'even' if _tag == 'odd' else 'odd'
 self.tag_add(_tag, _item)
 self.value_set(_TAGS, str(self.item(_item, 'tags')), _item)
 if int(self.item(_item, 'open')):
 for node in self.get_children(_item):
 _tag = set_tag(node, _tag)
 return _tag
 exclude = []
 if excluded and not isinstance(excluded, tk.Event):
 if isinstance(excluded, str):
 excluded = (excluded,)
 for item in excluded:
 if item in excluded:
 exclude.append(item)
 tag = 'odd'
 for item in self.get_children():
 reset(item)
 tag = set_tag(item, tag)
 self.value_set(_TAGS, str(self.item(item, 'tags')), item)
 def tag_replace(self, old, new, item=None):
 for item in (item,) if item else self.tag_has(old):
 if self.tag_has(old, item):
 self.tags_update('add', new, item)
 self.tags_update('remove', old, item)
 def tags_update(self, opt, tags, item):
 def get_items(node):
 items.append(node)
 for node in self.get_children(node):
 get_items(node)
 if not tags:
 return
 elif isinstance(tags, str):
 tags = (tags,)
 if not item:
 items = []
 for child in self.get_children():
 get_items(child)
 else:
 items = (item,)
 for item in items:
 _tags = list(self.item(item, 'tags'))
 for _tag in tags:
 if opt == 'add':
 if _tag not in _tags:
 _tags.append(_tag)
 elif opt == 'remove':
 if _tag in _tags:
 _tags.pop(_tags.index(_tag))
 self.item(item, tags=_tags)
 def value_get(self, idx, item):
 if not item:
 return ''
 values = list(self.item(item, 'values'))
 if 0 <= idx <= len(values):
 return values[idx]
 def value_set(self, idx, value, item):
 values = list(self.item(item, 'values'))
 if idx < len(values):
 values[idx] = value
 self.item(item, values=values)
 def dlg_rename(self, title, message, current_name):
 def skip(_=None):
 self.dlg_results = _SKIP
 dlg.destroy()
 def cancel(_=None):
 self.dlg_results = _CANCEL
 dlg.destroy()
 def rename(_=None):
 self.dlg_results = dlg.entry.var.get()
 dlg.destroy()
 root = self.winfo_toplevel()
 dlg = RenameDialog(root, width=320, height=150, title=title, message=message)
 dlg.update_idletasks()
 dlg.label.config(wraplength=dlg.container.winfo_width())
 dlg.button_rename.focus()
 dlg.entry.var.set(current_name)
 dlg.entry.select_range(0, tk.END)
 dlg.entry.icursor(tk.END)
 dlg.entry.focus_set()
 dlg.bind('<Return>', rename)
 dlg.bind('<KP_Enter>', rename)
 dlg.button_rename.bind('<Button-1>', rename)
 dlg.button_rename.bind('<Return>', rename)
 dlg.button_rename.bind('<KP_Enter>', rename)
 dlg.button_skip.bind('<Button-1>', skip)
 dlg.button_skip.bind('<Return>', skip)
 dlg.button_skip.bind('<KP_Enter>', skip)
 dlg.button_cancel.bind('<Button-1>', cancel)
 dlg.button_cancel.bind('<Return>', cancel)
 dlg.button_cancel.bind('<KP_Enter>', cancel)
 if self.active_popup_widget:
 x = self.active_popup_widget.winfo_rootx()
 y = self.active_popup_widget.winfo_rooty()
 else:
 bbox = self.bbox(self.focus())
 x, y, _, _ = bbox
 x += root.winfo_rootx()
 y += root.winfo_rooty()
 widest = 0
 font = tkfont.nametofont('TkDefaultFont')
 for node in self.get_children(self.focus()):
 size = font.measure(self.item(node, 'text'))
 if size > widest:
 widest = size + font.measure('W')
 x += (widest + font.measure('W') + self.indent * self.item_depth(self.focus()))
 y += self.rowheight + self.rowheight // 2
 dlg.geometry(f'{dlg.geometry().split("+", 1)[0]}+{x}+{y}')
 root.wait_window(dlg)
 return self.dlg_results
 def dlg_message(self, title, message):
 def ok(_=None):
 dlg.destroy()
 root = self.winfo_toplevel()
 dlg = MessageDialog(root, width=320, height=130, title=title, message=message)
 dlg.update_idletasks()
 dlg.label.config(wraplength=dlg.container.winfo_width())
 dlg.button_ok.focus()
 dlg.button_ok.config(command=ok)
 dlg.button_ok.bind('<Return>', ok)
 dlg.button_ok.bind('<KP_Enter>', ok)
 if self.active_popup_widget:
 x = self.active_popup_widget.winfo_rootx()
 y = self.active_popup_widget.winfo_rooty()
 else:
 bbox = self.bbox(self.focus())
 x, y, _, _ = bbox
 x += root.winfo_rootx()
 y += root.winfo_rooty()
 item = self.identify('item', x, y-self.winfo_rooty())
 widest = 0
 font = tkfont.nametofont('TkDefaultFont')
 for node in self.get_children(self.parent(item)):
 size = font.measure(self.item(node, 'text'))
 if size > widest:
 widest = size + font.measure('W')
 x += widest
 y += self.rowheight + self.rowheight // 2
 dlg.geometry(f'{dlg.geometry().split("+", 1)[0]}+{x}+{y}')
 root.wait_window(dlg)
 def cut(self, _=None):
 def set_selections(_item):
 self.tag_add('selected', _item)
 for _item in self.get_children(_item):
 set_selections(_item)
 selections = list(self.selection())
 for item in reversed(selections):
 if self.parent(item) in selections:
 selections.pop(selections.index(item))
 else:
 set_selections(item)
 item = self.focus()
 item = self.prev(item)
 if not item and self.get_children():
 item = self.get_children()[0]
 self.undo_data = {}
 for node in selections:
 self.undo_data[node] = (self.parent(node), self.index(node))
 self.detach(*selections)
 self.detached = selections
 self.focus(item)
 self.selection_add(item)
 self.tags_reset(excluded='selected')
 def copy(self, _=None):
 def set_selected(_item):
 self.selected.append(_item)
 self.tag_add('selected', _item)
 self.value_set(_TAGS, str(self.item(_item, 'tags')), _item)
 if not self.item(_item, 'open'):
 for node in self.get_children(_item):
 set_selected(node)
 if not self.shift:
 for item in self.tag_has('selected'):
 self.tag_remove('selected', item)
 self.value_set(_TAGS, str(self.item(item, 'tags')), item)
 self.selected = []
 for item in self.selection():
 set_selected(item)
 def undo(self, _=None):
 for item, (parent, idx) in self.undo_data.items():
 self.reattach(item, parent, idx)
 self.selection_remove(item)
 self.tags_reset()
 def paste(self, _=None):
 selections = self.detached if self.detached else self.selected
 if not self.selected and not self.detached:
 selections = self.tag_has('selected')
 for dst_item in self.selection():
 if not len(selections) or self.value_get(_TYPE, dst_item) != 'Node':
 continue
 if self.detached:
 for item in selections:
 self.reattach(item, dst_item, tk.END)
 self.detached = False
 else:
 selected = {}
 for item in selections:
 parent = self.parent(item)
 dst = selected[parent] if parent in selected else dst_item
 self.value_set(_MODIFIED, datetime.now().strftime("%Y/%m/%d %H:%M:%S"), item)
 iid = self.insert(dst, **self.item(item))
 if iid:
 self.value_set(_IID, iid, iid)
 self.tag_remove('selected', iid)
 selected[item] = iid
 self.tags_reset(excluded='selected')
 self.selection_remove(self.tag_has('selected'))
 self.selection_set(self.focus())
 def delete(self, *items):
 for item in items:
 parent = self.parent(item)
 if parent:
 value = int(self.value_get(_SIZE, parent).split(' ')[0])-1
 word = 'item' if value == 1 else 'items'
 self.value_set(_SIZE, f'{value} {word}', parent)
 super(Treeview, self).delete(*items)
 def insert(self, parent, index=tk.END, **kwargs):
 kwargs.pop('children', None)
 unique_columns = []
 for idx, c in enumerate(self.columns):
 if 'unique' in c and c['unique']:
 unique_columns.append(idx)
 for column in unique_columns:
 if column:
 pass
 else:
 text = kwargs['text']
 children = self.get_children(parent)
 column_values = []
 for node in children:
 column_values.append(self.item(node, 'text'))
 for node in children:
 while text == self.item(node, 'text'):
 result = self.dlg_rename(
 'Rename',
 f'The name "{text}" already exists, please choose another '
 f'name and try again.',
 text,
 )
 if result in (_SKIP, _CANCEL):
 return
 text = result
 kwargs['text'] = text
 iid = super(Treeview, self).insert(parent, index, **kwargs)
 child_count = len(self.get_children(parent))
 if child_count:
 word = 'item' if child_count == 1 else 'items'
 self.value_set(_SIZE, f'{len(self.get_children(parent))} {word}', parent)
 self.see(iid)
 return iid
 def escape(self, _):
 self.tags_reset()
 self.selection_remove(*self.selection())
 self.selection_set(self.focus())
 def control_a(self, _):
 def select(_child):
 self.selection_add(_child)
 for node in self.get_children(_child):
 select(node)
 for child in self.get_children():
 select(child)
 def shift_up(self, _):
 rowheight = self.style.lookup('Treeview', 'rowheight')
 focus = self.focus()
 x, y, _, _ = self.bbox(focus)
 x += self.winfo_rootx()
 _prev = self.identify('item', x, y-rowheight+1)
 if _prev:
 self.see(_prev)
 self.focus(_prev)
 self.cursor_offset += 1
 if self.cursor_offset > 0:
 self.selection_toggle(_prev)
 else:
 self.selection_toggle(focus)
 return 'break'
 def shift_down(self, _):
 rowheight = self.style.lookup('Treeview', 'rowheight')
 focus = self.focus()
 x, y, _, _ = self.bbox(focus)
 x += self.winfo_rootx()
 _next = self.identify('item', x, y+rowheight+1)
 if _next:
 self.see(_next)
 self.focus(_next)
 self.cursor_offset -= 1
 if self.cursor_offset >= 0:
 self.selection_toggle(focus)
 else:
 self.selection_toggle(_next)
 return 'break'
 def key_press(self, event):
 if 'Shift' in event.keysym:
 self.shift = True
 self.cursor_offset = 0
 def key_release(self, event):
 if 'Shift' in event.keysym:
 self.shift = False
 def expand_tree(self, _):
 def func():
 item = self.identify('item', self.winfo_pointerx(), self.winfo_pointery()-self.winfo_rooty())
 self.value_set(_OPEN, True, item)
 self.tags_reset(excluded='selected')
 self.after(1, func)
 def collapse_tree(self, _=None):
 def func():
 item = self.identify('item', self.winfo_pointerx(), self.winfo_pointery()-self.winfo_rooty())
 self.value_set(_OPEN, False, item)
 self.tags_reset(excluded='selected')
 self.after(1, func)
 def column_expand(self, event):
 def walk(_children):
 _largest = 0
 idx = int(column.lstrip('#'))-1
 for child in _children:
 if column == '#0':
 _text = self.item(child, 'text')
 elif len(self.item(child, 'values')) > 1:
 _text = self.item(child, 'values')[idx]
 else:
 continue
 _length = font.measure(_text) + (indent * self.item_depth(child)) if column == '#0' else font.measure(_text)
 if _length > _largest:
 _largest = _length
 _children = self.get_children(child)
 if not _children or not int(self.item(child, 'open')):
 continue
 _length = walk(_children)
 if _length > _largest:
 _largest = _length
 return _largest
 region = self.identify('region', event.x, event.y)
 if region != 'separator':
 return
 largest = 0
 column = self.identify('column', event.x, event.y)
 font = tkfont.nametofont('TkTextFont')
 font_width = font.measure('W')
 row_height = font.metrics('linespace')
 indent = row_height + font_width
 self.style.configure('Treeview', indent=indent)
 for item in self.get_children():
 text = self.item(item, 'text') if column == '#0' else self.item(item, 'values')[0]
 length = font.measure(text)+indent
 largest = length if length > largest else largest
 children = self.get_children(item)
 if not children or not int(self.item(item, 'open')):
 continue
 length = walk(children)
 if length > largest:
 largest = length
 self.column(column, width=largest+font_width)
 def detach(self, *items):
 if not items:
 items = self.selection()
 self.undo_data = {}
 for item in items:
 self.undo_data[item] = (self.parent(item), self.index(item))
 parent = self.parent(item)
 if parent:
 value = int(self.value_get(_SIZE, parent).split(' ')[0])-1
 word = 'item' if value == 1 else 'items'
 self.value_set(_SIZE, f'{value} {word}', parent)
 item = self.focus()
 item = self.prev(item)
 super(Treeview, self).detach(*self.selection())
 self.focus(item)
 self.selection_add(item)
 self.tags_reset(excluded='selected')
 def reattach(self, item, parent, index):
 for idx, column in enumerate(self.columns):
 if 'unique' in column and column['unique']:
 if idx:
 pass
 else:
 text = self.item(item, 'text')
 children = self.get_children(parent)
 column_values = []
 for node in children:
 column_values.append(self.item(node, 'text'))
 for node in children:
 while text == self.item(node, 'text'):
 result = self.dlg_rename(
 'Rename',
 f'The name "{text}" already exists, please choose another '
 f'name and try again.',
 text,
 )
 if result in (_SKIP, _CANCEL):
 return
 text = result
 self.item(item, text=text)
 iid = self.move(item, parent, index)
 return iid
 def wheel_mouse(self, event):
 if not self.item(self.focus(), 'text'):
 self.delete(self.focus())
 value = -0.1/3 if event.num == 5 else 0.1/3
 self.yview('moveto', self.yview()[0] + value)
 return 'break'
 def button_click(self, _):
 if self.active_popup_widget:
 item = self.focus()
 item_text = self.item(item, 'text')
 wdg_text = self.active_popup_widget.var.get().strip(' ')
 column = int(self.active_popup_column.lstrip('#'))
 unique = self.columns[column].get('unique', False)
 self.active_popup_widget.destroy()
 self.active_popup_widget = None
 if item_text == wdg_text and not item_text:
 self.delete(item)
 self.tags_reset()
 return
 if not item_text and not wdg_text:
 self.delete(item)
 self.tags_reset()
 return
 if not item_text:
 if unique:
 for node in self.get_children(self.parent(item)):
 if wdg_text == self.item(node, 'text'):
 self.delete(item)
 self.tags_reset()
 return
 else:
 return
 if unique:
 for node in self.get_children(self.parent(item)):
 if wdg_text == self.item(node, 'text'):
 return
 if not column and wdg_text:
 self.item(self.focus(), text=wdg_text)
 else:
 self.value_set(column-1, wdg_text, self.focus())
 self.active_popup_widget = None
 self.tags_reset()
 def button_release(self, event):
 self.focus(self.identify('item', event.x, event.y))
 def button_double_click(self, event):
 region = self.identify_region(event.x, event.y)
 if region == 'tree' or region == 'cell':
 row = self.identify_row(event.y)
 column = self.identify_column(event.x)
 wdg = self.active_popup_widget = self.popup_widget(row, column)
 if wdg:
 self.active_popup_column = column
 if isinstance(wdg, Entry):
 wdg.select_range(0, tk.END)
 elif region == 'separator':
 self.column_expand(event)
 elif region == 'heading':
 pass
 self.after(1, self.tags_reset)
 return 'break'
 def item_depth(self, item):
 depth = 1
 parent = self.parent(item)
 while parent:
 depth += 1
 parent = self.parent(parent)
 return depth
 def insert_node(self):
 item = self.identify('item', self.popup.x, self.popup.y-self.winfo_rooty())
 if not item:
 parent = ''
 idx = tk.END
 elif self.value_get(_TYPE, item) == 'Node':
 idx = 0
 parent = item
 else:
 idx = self.index(item) + 1
 parent = self.parent(item)
 iid = self.insert(
 parent,
 idx,
 open=True,
 **{'text': '', 'values': (['', 'Node', True, '', '', datetime.now().strftime("%Y/%m/%d %H:%M:%S"), ''])},
 )
 self.focus(iid)
 self.value_set(_IID, iid, iid)
 self.tags_reset()
 self.active_popup_widget = self.popup_widget(iid, '#0')
 def insert_leaf(self):
 item = self.identify('item', self.popup.x, self.popup.y-self.winfo_rooty())
 if not item:
 parent = ''
 idx = tk.END
 elif self.value_get(_TYPE, item) == 'Node':
 idx = 0
 parent = item
 else:
 idx = self.index(item) + 1
 parent = self.parent(item)
 iid = self.insert(
 parent,
 idx,
 **{'text': '', 'values': (['', 'Leaf', '', '', '0 Kb', datetime.now().strftime("%Y/%m/%d %H-%M-%S"), ''])},
 )
 self.focus(iid)
 self.value_set(_IID, iid, iid)
 self.tags_reset()
 self.active_popup_widget = self.popup_widget(iid, '#0')
 def populate(self, parent, data=()):
 for item in data:
 iid = self.insert(parent, tk.END, **item)
 self.value_set(_IID, iid, iid)
 if 'children' in item:
 self.populate(iid, item['children'])
 def serialize(self):
 def get_data(_item, _data):
 for node in self.get_children(_item):
 _item_data = self.item(node)
 _data.append(_item_data)
 if self.get_children(node):
 _item_data['children'] = []
 get_data(node, _item_data['children'])
 data = {'headings': self.headings, 'columns': self.columns, 'data': {}}
 tree_data = []
 for item in self.get_children():
 item_data = self.item(item)
 if self.get_children(item):
 item_data['children'] = []
 tree_data.append(item_data)
 get_data(item, item_data['children'])
 else:
 tree_data.append(item_data)
 data['data'] = tree_data
 return data
 def popup_menu(self, event):
 region = self.identify_region(event.x, event.y)
 if region == 'heading':
 return
 if self.active_popup_widget:
 self.active_popup_widget.destroy()
 self.active_popup_widget = None
 self.popup.x, self.popup.y = event.x_root, event.y_root
 item = self.identify('item', event.x, event.y)
 self.focus(item)
 self.focus_set()
 self.popup.tk_popup(event.x_root, event.y_root, 0)
 def popup_widget(self, row, column):
 if not row or not column:
 return
 bbox = self.bbox(row, column)
 if not bbox:
 return
 if self.active_popup_widget:
 self.active_popup_widget.destroy()
 x_pos, y_pos, width, height = self.bbox(row, column)
 item = self.identify('item', x_pos, y_pos+self.rowheight)
 y_pos += height // 2
 if column == '#0':
 col = 0
 text = self.item(item, 'text')
 x_pos += self.indent // 2
 width -= self.indent // 2 + 1
 else:
 col = int(column.lstrip('#'))
 text = self.value_get(col-1, item)
 x_pos += 1
 wdg = None
 mode = self.columns[col].get('mode', tk.WRITABLE)
 unique = self.columns[col].get('unique', False)
 _type = self.columns[col].get('type', None)
 if _type == 'Entry':
 def tab(_):
 if int(column.lstrip('#')) >= len(self.columns)-1:
 self.active_popup_widget = self.popup_widget(self.focus(), '#0')
 self.active_popup_column = '#0'
 else:
 for idx, data in enumerate(self.columns[int(column.lstrip('#'))+1:]):
 if 'type' in data:
 self.active_popup_widget = self.popup_widget(self.focus(), f'#{idx+1}')
 self.active_popup_column = f'#{idx+1}'
 self.active_popup_widget.focus_set()
 self.active_popup_widget.select_range(0, tk.END)
 return 'break'
 def update(_):
 _item = self.focus()
 wdg_text = wdg.var.get().strip(' ')
 item_text = self.item(_item, 'text')
 if item_text != wdg_text:
 if not item_text and not wdg_text:
 wdg.destroy()
 self.active_popup_widget = None
 self.delete(_item)
 return
 elif item_text and not wdg_text:
 self.item(_item, text=item_text)
 wdg.destroy()
 self.active_popup_widget = None
 self.focus_set()
 self.focus(_item)
 return
 if not col:
 if unique:
 parent = self.parent(_item)
 children = self.get_children(parent)
 column_values = []
 for node in children:
 column_values.append(self.item(node, 'text'))
 for node in children:
 while wdg_text == self.item(node, 'text'):
 result = self.dlg_rename(
 'Rename',
 f'The name "{wdg_text}" already exists, please choose another '
 f'name and try again.',
 wdg_text,
 )
 if result == '':
 continue
 if result in (_SKIP, _CANCEL):
 return
 wdg_text = result
 self.item(item, text=text)
 if wdg_text == self.item(node, 'text'):
 return
 self.item(_item, text=wdg_text)
 else:
 self.value_set(col-1, wdg.get(), _item)
 wdg.destroy()
 self.active_popup_widget = None
 self.tags_reset()
 self.focus_set()
 self.selection_set(_item)
 def destroy(_=None):
 wdg.destroy()
 self.active_popup_widget = None
 _item = self.focus()
 _text = self.item(_item, 'text')
 self.tags_reset()
 if not _text:
 self.delete(item)
 self.tags_reset()
 self.focus_set()
 def control_a(_=None):
 def func():
 wdg.select_range(0, tk.END)
 wdg.icursor(tk.END)
 self.after(1, func)
 def move_focus(event):
 if event.keysym == 'Up':
 update(event)
 _item = self.focus()
 wdg.destroy()
 self.active_popup_widget = None
 self.focus_set()
 prev = self.prev(self.focus())
 if prev:
 self.selection_set(prev)
 self.focus(prev)
 else:
 update(event)
 _item = self.focus()
 wdg.destroy()
 self.active_popup_widget = None
 self.focus_set()
 _next = self.next(self.focus())
 if _next:
 self.selection_set(_next)
 self.focus(_next)
 if mode == tk.WRITABLE:
 wdg = Entry(self)
 wdg.place(x=x_pos+4, y=y_pos, anchor='w', width=width-4)
 wdg.var.set(text)
 wdg.icursor(tk.END)
 wdg.focus()
 wdg.focus_set()
 bindings = {
 '<Up>': move_focus,
 '<Down>': move_focus,
 '<Tab>': tab,
 # '<Control-ISO_Left_Tab>': tab,
 '<Return>': update,
 '<KP_Enter>': update,
 '<Escape>': destroy,
 '<Control-z>': destroy,
 '<Control-a>': control_a,
 }
 for command, callback in bindings.items():
 wdg.bind(command, callback)
 elif _type == 'Combobox':
 def tab(_):
 if int(column.lstrip('#')) >= len(self.columns)-1:
 self.active_popup_widget = self.popup_widget(self.focus(), '#0')
 else:
 for idx, data in enumerate(self.columns[int(column.lstrip('#'))+1:]):
 if 'type' in data:
 self.active_popup_widget = self.popup_widget(self.focus(), f'#{idx+1}')
 self.active_popup_widget.focus_set()
 self.active_popup_widget.select_range(0, tk.END)
 return 'break'
 def update(_):
 _text = wdg.get().strip(' ')
 if not col:
 self.item(self.focus(), text=_text)
 else:
 self.value_set(col-1, _text, self.focus())
 destroy()
 self.focus_set()
 def destroy(_=None):
 wdg.destroy()
 self.active_popup_widget = None
 self.focus_set()
 def control_a(_=None):
 def func():
 wdg.select_range(0, tk.END)
 wdg.icursor(tk.END)
 self.after(1, func)
 state = '' if mode == tk.WRITABLE else 'readonly'
 values = self.columns[col].get('values', '')
 wdg = Combobox(self, state=state, values=values)
 wdg.place(x=x_pos, y=y_pos, anchor='w', width=width-2)
 wdg.var.set(text)
 wdg.icursor(tk.END)
 bindings = {
 '<Tab>': tab,
 # '<Control-ISO_Left_Tab>': tab,
 '<Return>': update,
 '<KP_Enter>': update,
 '<Escape>': destroy,
 '<Control-z>': destroy,
 '<Control-a>': control_a,
 }
 for command, callback in bindings.items():
 wdg.bind(command, callback)
 return wdg
 def popup_widget_edit(self, _):
 self.active_popup_widget = self.popup_widget(self.focus(), '#0')
 self.active_popup_widget.select_range(0, tk.END)
 self.active_popup_widget.icursor(tk.END)
 self.active_popup_widget.focus_set()
 self.active_popup_column = '#0'
 return 'break'
 def popup_destroy(self, _):
 if self.active_popup_widget:
 self.active_popup_widget.destroy()
 self.active_popup_widget = None
 def bindings_set(self):
 bindings = {
 '<Up>': self.popup_destroy,
 '<Down>': self.popup_destroy,
 '<Key>': self.key_press,
 '<Tab>': self.popup_widget_edit,
 '<Escape>': self.escape,
 '<Return>': self.popup_widget_edit,
 '<KP_Enter>': self.popup_widget_edit,
 '<Button-1>': self.button_click,
 '<Button-4>': self.wheel_mouse,
 '<Button-5>': self.wheel_mouse,
 '<Shift-Up>': self.shift_up,
 '<Shift-Down>': self.shift_down,
 '<Control-a>': self.control_a,
 '<Control-x>': self.cut,
 '<Control-c>': self.copy,
 '<Control-v>': self.paste,
 '<Control-z>': self.undo,
 '<KeyRelease>': self.key_release,
 '<ButtonPress-3>': self.popup_menu,
 '<Double-Button-1>': self.button_double_click,
 '<ButtonRelease-1>': self.button_release,
 '<<TreeviewOpen>>': self.expand_tree,
 '<<TreeviewClose>>': self.collapse_tree,
 }
 for command, callback in bindings.items():
 self.bind(command, callback)
def main():
 app = App()
 app.mainloop()
if __name__ == '__main__':
 main()
asked Jun 13, 2020 at 2:42
\$\endgroup\$
1
  • \$\begingroup\$ I like test cases that exemplify usage. And doc strings. \$\endgroup\$ Commented Jun 13, 2020 at 5:48

1 Answer 1

3
\$\begingroup\$

Enumerations

You're 99% of the way to a useful Enum, here:

_IID = 0
_TYPE = 1
_OPEN = 2
_TAGS = 3
_SIZE = 4
_MODIFIED = 5
_DATA1 = 6

I'm not really clear on what they're used for - maybe NodeKey? Put them in an Enum.

No-op if

 if platform == "linux" or platform == "linux2":
 self.platform = 'linux'

doesn't need the first predicate; simply

if platform == 'linux2':
 self.platform = 'linux'

or even

if 'linux' in platform:
 self.platform = 'linux'

However, it seems you don't even use self.platform, so the works could get deleted.

pathlib

Rather than using path.join, consider using pathlib.Path, which has a much nicer object-oriented interface for path manipulation.

Default for get

settings.get('focus', None)

does not need to write None since that is the default.

Multi-assignment

 self.popup = \
 self.menu_background = None

doesn't really have any advantages; just use individual assignment.

Single dict

 bindings = {
 '<ButtonPress-3>': self.popup_menu,
 }
 for command, callback in bindings.items():
 self.bind(command, callback)

is odd. Why not just

self.bind('<ButtonPress-3>', self.popup_menu)

?

Even if you had a long series of bindings, a dict would not be appropriate - you could just use a tuple of tuples.

More if logic

 if idx:
 pass
 else:

should just be

if not idx:

Event numbers

event.num == 5

is mysterious. I'd be surprised if tk did not have a constant for this already, but if it doesn't, you should declare one.

kwargs

 iid = self.insert(
 parent,
 idx,
 open=True,
 **{'text': '', 'values': (['', 'Node', True, '', '', datetime.now().strftime("%Y/%m/%d %H:%M:%S"), ''])},
 )

In this case, there is no advantage to using a dict. Just use regular unquoted kwargs.

answered Jun 13, 2020 at 16:19
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thank you for your input, I'll make them recommendations happen. \$\endgroup\$ Commented Jun 13, 2020 at 16:33
  • \$\begingroup\$ I have made all the changes you suggested thanks once again. I have updated the code if you have the time please look at it and let me know if I have accomplished the things you spoke of. github.com/unodan/TkInter-Treeview-Example-Demo \$\endgroup\$ Commented Jun 13, 2020 at 21:46

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.