Tkinter Listbox with Entry

en_Knight picture en_Knight · Jul 30, 2013 · Viewed 8.7k times · Source

Is there a way to have the items of a Tkinter Listbox be Entry Widgets? The result would be that you could dynamically modify the text in an Listbox entry. If your Listbox looks like:

 --------
| Apples  |
| Pears   |
| Oranges |
 ---------

then you would want to be able to click on Apples and write some arbitrary text - you could then bind the Enter key, say, to trigger a function based on the new text.

Answer

David Duran picture David Duran · May 26, 2017

I know it has been a while since this question, but I have created a widget called 'ListboxEditable', which is able to act as a listbox and, when double-clicking on an item, the user can type anything inside an entry. Then, when the user clicks another row, the information is saved on the corresponding modified cell. Note that the user can use the up and down keys to browse the entire given list (the selected row has a different background color).

This code has been developed based on the answer from @Bryan Oakley.

Minimal working case

# Imports
from tkinter import *
from tkinter.ttk import *
# Import for the listboxEditable
from ListboxEditable import *

# Colors
colorActiveTab="#CCCCCC" # Color of the active tab
colorNoActiveTab="#EBEBEB" # Color of the no active tab
# Fonts
fontLabels='Calibri'
sizeLabels2=13

# Main window
root = Tk()

# *** Design *****
frame_name=Frame(root,bg=colorActiveTab) # Column frame
frame_name_label=Frame(frame_name,bg='blue') # Label frame
label_name=Label(frame_name_label, text="Header", bg='blue', fg='white', font=(fontLabels, sizeLabels2, 'bold'), pady=2, padx=2, width=10)
frame_name_listbox=Frame(frame_name,bg='blue') # Label frame
list_name=['test1','test2','test3']
listBox_name=ListboxEditable(frame_name_listbox,list_name)

# *** Packing ****
frame_name.pack(side=LEFT,fill=Y)
frame_name_label.pack(side=TOP, fill=X)
label_name.pack(side=LEFT,fill=X)
frame_name_listbox.pack(side=TOP, fill=X)
listBox_name.placeListBoxEditable()

# Infinite loop
root.mainloop()

ListboxEditable class

# Author: David Duran Perez
# Date: May 26, 2017

# Necessary imports
from tkinter import *
from tkinter import ttk

# Colors
colorActiveTab="#CCCCCC" # Color of the active tab
colorNoActiveTab="#EBEBEB" # Color of the no active tab
# Fonts
fontLabels='Calibri'
sizeLabels2=13

class ListboxEditable(object):
    """A class that emulates a listbox, but you can also edit a field"""
    # Constructor
    def __init__(self,frameMaster,list):
        # *** Assign the first variables ***
        # The frame that contains the ListboxEditable
        self.frameMaster=frameMaster
        # List of the initial items
        self.list=list
        # Number of initial rows at the moment
        self.numberRows=len(self.list)

        # *** Create the necessary labels ***
        ind=1
        for row in self.list:
            # Get the name of the label
            labelName='label'+str(ind)
            # Create the variable
            setattr(self, labelName, Label(self.frameMaster, text=self.list[ind-1], bg=colorActiveTab, fg='black', font=(fontLabels, sizeLabels2), pady=2, padx=2, width=10))

            # ** Bind actions
            # 1 left click - Change background
            getattr(self, labelName).bind('<Button-1>',lambda event, a=labelName: self.changeBackground(a))
            # Double click - Convert to entry
            getattr(self, labelName).bind('<Double-1>',lambda event, a=ind: self.changeToEntry(a))
            # Move up and down
            getattr(self, labelName).bind("<Up>",lambda event, a=ind: self.up(a))
            getattr(self, labelName).bind("<Down>",lambda event, a=ind: self.down(a))

            # Increase the iterator
            ind=ind+1

    # Place
    def placeListBoxEditable(self):
        # Go row by row placing it
        ind=1
        for row in self.list:
            # Get the name of the label
            labelName='label'+str(ind)
            # Place the variable
            getattr(self, labelName).grid(row=ind-1,column=0)

            # Increase the iterator
            ind=ind+1


    # Action to do when one click
    def changeBackground(self,labelNameSelected):
        # Ensure that all the remaining labels are deselected
        ind=1
        for row in self.list:
            # Get the name of the label
            labelName='label'+str(ind)
            # Place the variable
            getattr(self, labelName).configure(bg=colorActiveTab)

            # Increase the iterator
            ind=ind+1

        # Change the background of the corresponding label
        getattr(self, labelNameSelected).configure(bg=colorNoActiveTab)
        # Set the focus for future bindings (moves)
        getattr(self, labelNameSelected).focus_set()


    # Function to do when up button pressed
    def up(self, ind):
        if ind==1: # Go to the last
            # Get the name of the label
            labelName='label'+str(self.numberRows)
        else: # Normal
            # Get the name of the label
            labelName='label'+str(ind-1)

        # Call the select
        self.changeBackground(labelName)


    # Function to do when down button pressed
    def down(self, ind):
        if ind==self.numberRows: # Go to the last
            # Get the name of the label
            labelName='label1'
        else: # Normal
            # Get the name of the label
            labelName='label'+str(ind+1)

        # Call the select
        self.changeBackground(labelName)


    # Action to do when double-click
    def changeToEntry(self,ind):
        # Variable of the current entry
        self.entryVar=StringVar()
        # Create the entry
        #entryName='entry'+str(ind) # Name
        self.entryActive=ttk.Entry(self.frameMaster, font=(fontLabels, sizeLabels2), textvariable=self.entryVar, width=10)
        # Place it on the correct grid position
        self.entryActive.grid(row=ind-1,column=0)
        # Focus to the entry
        self.entryActive.focus_set()

        # Bind the action of focusOut
        self.entryActive.bind("<FocusOut>",lambda event, a=ind: self.saveEntryValue(a))

    
    # Action to do when focus out from the entry
    def saveEntryValue(self,ind):
        # Find the label to recover
        labelName='label'+str(ind)
        # Remove the entry from the screen
        self.entryActive.grid_forget()
        # Place it again
        getattr(self, labelName).grid(row=ind-1,column=0)
        # Change the name to the value of the entry
        getattr(self, labelName).configure(text=self.entryVar.get())

Some sreenshots

enter image description here

enter image description here

enter image description here