I am creating an autocomplete popup in Python using a Listbox which pops up. One can navigate by using arrows and using Enter to select or use the mouse to select an option. The arrow navigation is working perfectly; however, when I use the mouse to select an option and I want to use the arrows again, it automatically selects the second item on the list. Here is the code:
import tkinter as tk
masterlist = ["Daniel", "Ronel", "Dana", "Elani", "Isabel", "Hercules", "Karien", "Amor", "Piet", "Koos", "Jan", "Johan", "Denise", "Jean", "Petri"]
global listbox_wiggie_index
def new_list_for_listbox(*args):
# When text in entry.wiggie is changed,
# "search_var = tk.StringVar()" will change, which will activate this function.
global listbox_wiggie_index
listbox_wiggie_index = -1 # Reset index every time entry_wiggie changes so listbox is updated
# Clean listbox_wiggie
listbox_wiggie.delete(0, tk.END)
# Get value of changeable search_var.
t = search_var.get()
if t == "":
# Hide listbox_wiggie
# If "listbox_wiggie.destroy()" is used, the widget will have to be created again
listbox_wiggie.place_forget()
return
else:
# Populate listbox_wiggie
t = search_var.get()
for e in masterlist:
if t.lower() in e.lower():
listbox_wiggie.insert(tk.END, e)
place_listbox_wiggie()
print(f"|new_list_for_listbox|New list was generated and listbox_wiggie was placed. Index = {listbox_wiggie_index}")
def place_listbox_wiggie():
# The listbox_wiggie should be already populated. It will be placed at the bottom of entry_wiggie
# Get coordinates of bottom left of entry_wiggie.
x = entry_wiggie.winfo_rootx() - root.winfo_rootx()
y = entry_wiggie.winfo_rooty() - root.winfo_rooty() + entry_wiggie.winfo_height()
# Place listbox_wiggie at the bottom of entry_wiggie
listbox_wiggie.place(x=x, y=y)
entry_wiggie.focus_set()
def listbox_mouse_selection(event=None):
# From: "listbox_wiggie.bind("<<ListboxSelect>>", listbox_mouse_selection)"
i = listbox_wiggie.curselection()[0] # i = integer
t = listbox_wiggie.get(i) # text of selection
print("|listbox_mouse_selection| Selection was made using mouse")
write_value_to_label(t)
def write_value_to_label(teks):
# Write selection to label_wiggie
global listbox_wiggie_index
label_wiggie.config(text=teks)
# Hide listbox_wiggie
listbox_wiggie.place_forget()
entry_wiggie.focus_set()
entry_wiggie.delete(0, tk.END)
listbox_wiggie_index = -1
print(f"|write_value_to_label|{teks} was written to label_wiggie")
def change_listbox_selection_after_arrow_press():
# Focus on listbox_wiggie after arrows are pressed
global listbox_wiggie_index
# Remove the selection highlight from every item in the Listbox, starting from index 0 to the last item (tk.END)
listbox_wiggie.selection_clear(0, tk.END)
# Select the new item according to the index "listbox_wiggie_index":
listbox_wiggie.selection_set(listbox_wiggie_index)
# Highlight the item as the active item:
listbox_wiggie.activate(listbox_wiggie_index)
print(f'|change_listbox_selection_after_arrow_press|Selector of listbox_wiggie was changed to = {listbox_wiggie_index}')
def on_entry_wiggie_key_press(event):
# Function is activated by: entry_wiggie.bind("<KeyRelease>", on_entry_wiggie_key_press)
global listbox_wiggie_index
if listbox_wiggie.size() > 0:
if event.keysym == "Down":
print(f'|on_entry_wiggie_key_press|Down was pressed. Before pressing, index = {listbox_wiggie_index}')
listbox_wiggie_index += 1
if listbox_wiggie_index >= listbox_wiggie.size():
listbox_wiggie_index = 0
change_listbox_selection_after_arrow_press()
elif event.keysym == "Up":
print('|on_entry_wiggie_key_press|Up was pressed. Before pressing, index = {listbox_wiggie_index}')
listbox_wiggie_index -= 1
if listbox_wiggie_index < 0:
listbox_wiggie_index = listbox_wiggie.size() - 1
change_listbox_selection_after_arrow_press()
elif event.keysym == "Return":
print('|on_entry_wiggie_key_press|Return was pressed.')
# check to ensure listbox_wiggie_index is valid before calling listbox_wiggie.get()
if 0 <= listbox_wiggie_index < listbox_wiggie.size():
write_value_to_label(listbox_wiggie.get(listbox_wiggie_index))
elif event.keysym == "Escape":
print('|on_entry_wiggie_key_press|Escape was pressed.')
# Clear the listbox
listbox_wiggie.delete(0, tk.END)
listbox_wiggie.place_forget()
entry_wiggie.focus_set()
else:
print(f'|on_entry_wiggie_key_press|{event.keysym} was pressed.')
listbox_wiggie_index = -1
else:
print(f'|on_entry_wiggie_key_press|{event.keysym} was pressed.')
listbox_wiggie_index = -1
print(f'|on_entry_wiggie_key_press|search_var = "{search_var.get()}". Focus = {root.focus_get()}. Indeks = {listbox_wiggie_index}. ...curselection()[0] = {listbox_wiggie.curselection()}')
print()
root = tk.Tk()
root.title("Listbox appear after typing")
root.geometry("400x200")
# search_var is a variable which holds the content of entry_wiggie.
search_var = tk.StringVar()
search_var.trace("w", new_list_for_listbox)
# new_list_for_listbox will only be activated when search_var is changed (when contents of entry_wiggie is changed)
# Create entry_wiggie. Link content to StringVar "search_var"
entry_wiggie = tk.Entry(root, textvariable=search_var, width=60)
entry_wiggie.pack(pady=5)
entry_wiggie.focus_set()
entry_wiggie.bind("<KeyRelease>", on_entry_wiggie_key_press)
# Create listbox_wiggie, but do not place it (yet)
listbox_wiggie = tk.Listbox(root)
listbox_wiggie.bind("<<ListboxSelect>>", listbox_mouse_selection)
listbox_wiggie_index = -1
# Create label_wiggie to display result
label_wiggie = tk.Label(root, text="No option is selected.")
label_wiggie.pack(pady=5)
root.mainloop()
The main problem is in this line:
listbox_wiggie.bind("<<ListboxSelect>>", listbox_mouse_selection)
if you change this to:
listbox_wiggie.bind("<ButtonRelease-1>", listbox_mouse_selection)
Everything works!
Why?
There are two main concepts, active item and selection.
Active item is the one you change with keyboard navigation (Up and Down), which is also highlighted.
The selection is what the user explicitly chooses (like with a mouse click).
<<ListboxSelect>>
gets triggered when the selection changes. Meaning it gets triggered when the mouse clicks on an item.
Even though that might look fine, the core problem is that if a selection exists, the arrows change the selection, NOT the active item like it usually does. Meaning the listbox_mouse_selection
function gets triggered, which causes it to 'autocomplete' an item in the list.
Unlike <<ListboxSelect>>
, the <ButtonRelease-1>
only triggers when the left mouse button is released, meaning it does not trigger when the arrow keys change the selection which fixes the bug.
Feel free to ask any questions!