Search code examples
pythonttk

Standard ttk way to provide a list box with editing


I'm going to need a list control with multiple selection and a way for the user to edit the displayed list.

Judging by Python - python-list - ttk Listbox, ttk.Treeview is the new black way to display a list and a replacement for Tkinter.Listbox.

Is there some stock/recommended way provided to incorporate list editing, too?

This is typically done with three buttons "add"/"edit"/"delete" somewhere around the control (the first two may cause a small window with an edit control to pop up), or a list entry itself becomes an editor e.g. on a double click. Implementing either logic by hand would be nontrivial.


Solution

  • Tk and thus Tkinter only provide base widgets; reusable combinations of them are beyond their scope. The abandoned Tix package offered a mechanism for and a collection of these, called "mega-widgets", but the idea didn't catch on apparently.

    I did it my way the following way (using the Model-View-Presenter design pattern).

    It looks like this:

    how it looks

    import tkinter
    from tkinter import ttk
    from tkinter import W, E
    
    
    class View_EditableList(object):
        def __init__(self,root,root_row):
            """ List with buttons to edit its contents.
            :param root: parent widget
            :param roow_row: row in `root'. The section takes 2 rows.
            """
    
            self.list = tkinter.Listbox(root, selectmode=tkinter.EXTENDED)
            self.list.grid(row=root_row, sticky=(W, E))
            root.rowconfigure(root_row, weight=1)
    
            self.frame = ttk.Frame(root)
            self.frame.grid(row=root_row+1, sticky=(W, E))
    
            self.add = ttk.Button(self.frame, text="+", width=5)
            self.add.grid(row=0, column=1)
            self.edit = ttk.Button(self.frame, text="*", width=5)
            self.edit.grid(row=0, column=2)
            self.del_ = ttk.Button(self.frame, text="-", width=5)
            self.del_.grid(row=0, column=3)
            self.up = ttk.Button(self.frame, text=u"↑", width=5)
            self.up.grid(row=0, column=4)
            self.down = ttk.Button(self.frame, text=u"↓", width=5)
            self.down.grid(row=0, column=5)
            self.frame.grid_columnconfigure(0, weight=1)
            self.frame.grid_columnconfigure(6, weight=1)
    
    
    class Presenter_EditableList(object):
        def __init__(self,view,root):
            """
    
            :param view: View_EditableList
            :param root: root widget to be used as parent for modal windows
            """
            self.root = root
            self.view = view
            view.add.configure(command=self.add)
            view.edit.configure(command=self.edit)
            view.del_.configure(command=self.del_)
            view.up.configure(command=self.up)
            view.down.configure(command=self.down)
    
        def add(self):
            w=View_AskText(self.root)
            self.root.wait_window(w.top)
            if w.value:
                self.view.list.insert(self.view.list.size(),w.value)
    
        def edit(self):
            l=self.view.list
            try:
                [index]=l.curselection()
            except ValueError:
                return
            w=View_AskText(self.root,l.get(index))
            self.root.wait_window(w.top)
            if w.value:
                l.delete(index)
                l.insert(index,w.value)
    
        def del_(self):
            l=self.view.list
            try:
                [index]=l.curselection()
            except ValueError:
                return
            l.delete(index)
            l.select_set(max(index,l.size()-1))
    
        def up(self):
            l = self.view.list
            try:
                [index] = l.curselection()
            except ValueError:
                return
            if index>0:
                v = l.get(index)
                l.delete(index)
                l.insert(index-1,v)
                l.select_set(index-1)
    
        def down(self):
            l = self.view.list
            try:
                [index] = l.curselection()
            except ValueError:
                return
            if index<l.size()-1:
                v = l.get(index)
                l.delete(index)
                l.insert(index+1,v)
                l.select_set(index+1)
    
        def getlist(self):
            return [self.view.list.get(i) for i in range(self.view.list.size())]
    
        def setlist(self,list_):
            self.view.list.delete(0,tkinter.END)
            for i,v in enumerate(list_):
                self.view.list.insert(i,v)
    
    # supplemental class; it's in another file in my actual code
    class View_AskText(object):
        """
        A simple dialog that asks for a text value.
        """
        def __init__(self, master, value=u""):
            self.value = None
    
            top = self.top = tkinter.Toplevel(master)
            top.grab_set()
            self.l = ttk.Label(top, text=u"Value:")
            self.l.pack()
            self.e = ttk.Entry(top)
            self.e.pack()
            self.b = ttk.Button(top, text='Ok', command=self.save)
            self.b.pack()
    
            if value: self.e.insert(0, value)
            self.e.focus_set()
            top.bind('<Return>', self.save)
    
        def save(self, *_):
            self.value = self.e.get()
            self.top.destroy()
    
    
    root = tkinter.Tk()
    view = View_EditableList(root, 5)
    Presenter_EditableList(view, root)
    root.mainloop()