exec85
exec85

Reputation: 487

Python Tkinter listbox text edit in GUI

Is there a way to edit a listbox item instead deleting it?

I am using a tkinter GUI with a listbox. Now my listbox could contain for example 10 items. If I would like to change the text of one item, is there a way to do it?

I know I can create a button to delete a single item, but is there any way to also jut edit an item?

Upvotes: 1

Views: 2697

Answers (3)

Priyanshu
Priyanshu

Reputation: 127

modified version fo code shared by @Bryan Oakley, this version removes the scrollbar when editing is started and place back the scrollbar after editing is cancelled or done

import tkinter as tk
class EditableListbox:
    def __init__(self):
        self.editItemIndex = None
        self.root = tk.Tk()
        self.lb = tk.Listbox(self.root)
        self.vsb = tk.Scrollbar(self.root, command=self.lb.yview)
        self.vsb.pack(side="right", fill="y")
        self.lb.configure(yscrollcommand=self.vsb.set)
        self.lb.pack(side="left", fill="both", expand=True)
        for i in range(100):
            self.lb.insert("end", f"Item #{i + 1}")
        self.lb.bind("<Double-1>", self.startEdit)

    def startEdit(self, event):
        index = self.lb.index(f"@{event.x},{event.y}")
        self.vsb.pack_forget()
        self._start_edit(index)
        return "break"

    def _start_edit(self, index):
        self.editItemIndex = index
        text = self.lb.get(index)
        y0 = self.lb.bbox(index)[1]
        entry = tk.Entry(self.root, borderwidth=0, highlightthickness=1)
        entry.bind("<Return>", self.accept_edit)
        entry.bind("<Escape>", self.cancel_edit)
        entry.insert(0, text)
        entry.selection_from(0)
        entry.selection_to("end")
        entry.place(relx=0, y=y0, relwidth=1, width=-20)
        entry.focus_set()
        entry.grab_set()

    def cancel_edit(self, event):
        event.widget.destroy()
        self.vsb.pack(side="right", fill="y")

    def accept_edit(self, event):
        new_data = event.widget.get()
        self.lb.delete(self.editItemIndex)
        self.lb.insert(self.editItemIndex, new_data)
        event.widget.destroy()
        self.vsb.pack(side="right", fill="y")

    def runGUI(self):
        self.root.mainloop()
if __name__ == "__main__":
    flc_app = EditableListbox()
    flc_app.runGUI()

Upvotes: 2

Tom Smith
Tom Smith

Reputation: 1

Another variant, when dynamically populating listbox items from events, with use of EditableListbox(). If the listbox item is outside the visible area bbox() looks for, it raises a NoneType error on self.bbox(index)[1]. This is due to the newly added item being outside the visible window.

The below simply checks if the item is visible and calls see(index) respectively. However, it will manipulate the original visible state of the Listbox items (in regards to vscroll).

#TODO: Incorporate bbox() state preservation and restore after.

class EditableListbox(tk.Listbox):
    """A listbox where you can directly edit an item via double-click
listbox-text-edit-in-gui """
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self.edit_item = None
        self.bind("<Double-1>", self._start_edit)

    def _start_edit(self, event):
        index = self.index(f"@{event.x},{event.y}")
        self.start_edit(index)
        return "break"

    def start_edit(self, index, accept_func=None,cancel_func=None):
        if self.bbox(index) == None:
            self.see(index)

        self.edit_item = index
        text = self.get(index)
        y0 = self.bbox(index)[1]
        entry = tk.Entry(self, borderwidth=0, highlightthickness=1)
        entry.bind("<Return>", self.accept_edit)
        entry.bind("<Escape>", self.cancel_edit)

        entry.insert(0, text)
        entry.selection_from(0)
        entry.selection_to("end")
        entry.place(relx=0, y=y0, relwidth=1, width=-1)
        entry.focus_set()
        entry.grab_set()

    def cancel_edit(self, event):
        event.widget.destroy()

    def accept_edit(self, event):
        new_data = event.widget.get()
        self.delete(self.edit_item)
        self.insert(self.edit_item, new_data)
        event.widget.destroy()

Upvotes: 0

Bryan Oakley
Bryan Oakley

Reputation: 385900

The listbox doesn't support directly editing an item inside the listbox. You have to provide a mechanism for the user to enter data, and then you can replace just a single item in the listbox.

That being said, tkinter gives you all the tools you need to let the user double-click on an item and change it. Here's a quick hack of an example. In a nutshell, when you double-click, it will superimpose an entry widget over the item you clicked on, and when you press the return key it saves the item to the listbox.

import tkinter as tk

class EditableListbox(tk.Listbox):
    """A listbox where you can directly edit an item via double-click"""
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self.edit_item = None
        self.bind("<Double-1>", self._start_edit)

    def _start_edit(self, event):
        index = self.index(f"@{event.x},{event.y}")
        self.start_edit(index)
        return "break"

    def start_edit(self, index):
        self.edit_item = index
        text = self.get(index)
        y0 = self.bbox(index)[1]
        entry = tk.Entry(self, borderwidth=0, highlightthickness=1)
        entry.bind("<Return>", self.accept_edit)
        entry.bind("<Escape>", self.cancel_edit)

        entry.insert(0, text)
        entry.selection_from(0)
        entry.selection_to("end")
        entry.place(relx=0, y=y0, relwidth=1, width=-1)
        entry.focus_set()
        entry.grab_set()

    def cancel_edit(self, event):
        event.widget.destroy()

    def accept_edit(self, event):
        new_data = event.widget.get()
        self.delete(self.edit_item)
        self.insert(self.edit_item, new_data)
        event.widget.destroy()

root = tk.Tk()
lb = EditableListbox(root)
vsb = tk.Scrollbar(root, command=lb.yview)
lb.configure(yscrollcommand=vsb.set)

vsb.pack(side="right", fill="y")
lb.pack(side="left", fill="both", expand=True)

for i in range(100):
    lb.insert("end", f"Item #{i+1}")

root.mainloop()

screenshot

Upvotes: 5

Related Questions