I want to have an autofilter combobox in ttkbootstrap (FilteredCombobox). When you type any character in the entry box, the options list is shown just with the options that contain these characteres. I built it in base on a combination of an entry widget, a button, and a listbox (in a toplevel window). I tested it in a Main window (Window) and it runs fine but when I test it in a toplevel window it's not running. I have problem/conflit with the the TopLevel window that manages the application widgets and the TopLevel window that manages the listbox include in the FilteredCombobox. When you open the main toplevel window containing a FilteredCombobox, the Toplevel window included in the FilteredCombobox appears behind the main toplevel window. Then, when you push the button to show the list options the toplevel window con the listbox appears in the right site but without the focus. I cannot choose any option of the listbox.
import ttkbootstrap as tb
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
from tkinter import Listbox
class FilteredCombobox(tb.Frame):
def __init__(self, master, height=7, width=20, **kwargs):
super().__init__(master, **kwargs)
self.values = []
self.listbox_height = height
self.entry_width = width
self.showing_listbox_from_set = False
self.entry_var = tb.StringVar()
self.entry_frame = tb.Frame(self)
self.entry = tb.Entry(self.entry_frame, textvariable=self.entry_var, width=self.entry_width)
self.entry.pack(side=tb.LEFT, fill=tb.BOTH, expand=True)
self.entry.bind('<KeyRelease>', self.on_keyrelease)
self.bind("<FocusOut>", lambda event: self.validate_selection(master))
self.drop_button = tb.Button(self.entry_frame, text='▼', width=3,command=self.toggle_listbox, takefocus=False, padding=(0,5,0,5), bootstyle="primary-outline")
self.drop_button.pack(side=tb.RIGHT, padx=0, fill=tb.Y)
self.entry_frame.pack(fill=tb.BOTH)
self.listbox_top = tb.Toplevel(self)
self.listbox_top.withdraw()
self.listbox_top.overrideredirect(True)
self.listbox = Listbox(self.listbox_top, height=self.listbox_height, width=self.entry_width)
self.listbox.pack()
self.listbox.bind('<<ListboxSelect>>', self.on_select)
self.listbox.bind('<Motion>', self.on_motion)
self.entry.bind('<FocusOut>', self.hide_listbox)
self.listbox_top.bind('<FocusOut>', self.hide_listbox)
self.entry.bind('<Tab>', self.handle_tab)
self.entry.bind('<Return>', self.fill_with_first_option)
self.entry_var.trace('w', self.update_list)
def toggle_listbox(self):
if self.listbox_top.winfo_viewable():
self.hide_listbox()
else:
self.show_listbox()
def show_listbox(self):
self.update_list(full_list=True)
self.position_listbox()
self.listbox_top.deiconify()
self.listbox.selection_clear(0, tb.END)
def on_keyrelease(self, event):
if event.keysym in ("Tab", "Return", "Up", "Down", "Left", "Right"):
return
self.update_list()
def update_list(self, *args, full_list=False):
typed_text = self.entry_var.get().lower()
self.listbox.delete(0, tb.END)
matches = self.values if full_list else [value for value in self.values if typed_text in value.lower()]
for value in matches:
self.listbox.insert(tb.END, value)
if matches and not self.showing_listbox_from_set:
self.position_listbox()
self.listbox_top.deiconify()
self.listbox.selection_set(0)
else:
self.listbox_top.withdraw()
def position_listbox(self):
x = self.entry_frame.winfo_rootx()
y = self.entry_frame.winfo_rooty() + self.entry_frame.winfo_height()
self.listbox_top.geometry(f"+{x}+{y}")
def on_select(self, event):
if self.listbox.curselection():
selected = self.listbox.get(self.listbox.curselection())
self.entry_var.set(selected)
self.hide_listbox()
def on_motion(self, event):
index = self.listbox.nearest(event.y)
self.listbox.selection_clear(0, tb.END)
self.listbox.selection_set(index)
def hide_listbox(self, event=None):
self.listbox_top.withdraw()
def handle_tab(self, event):
if self.listbox_top.winfo_viewable():
self.fill_with_first_option(event)
self.showing_listbox_from_set = True
self.after(100, lambda: setattr(self, "showing_listbox_from_set", False))
def fill_with_first_option(self, event):
if self.listbox.size() > 0 and self.listbox.curselection():
self.on_select(None)
self.entry.focus_set()
return 'break'
def get(self):
return self.entry_var.get()
def set(self, value):
self.entry_var.set(value)
self.showing_listbox_from_set =
self.update_list()
self.showing_listbox_from_set = False
def set_completion_list(self, lista):
lista.sort()
self.values = lista
self.entry_var.set('')
self.listbox.delete(0, tb.END)
for value in lista:
self.listbox.insert(tb.END, value)
def current(self, index):
if 0 <= index < len(self.values):
self.entry_var.set(self.values[index])
self.update_list()
self.listbox.selection_clear(0, tb.END)
self.listbox.selection_set(index)
self.listbox.see(index)
else:
return
def validate_selection(self, ventana):
selected_value = self.get()
if selected_value != '':
if selected_value not in self.values:
Messagebox.show_warning("Debes elegir una de las opciones disponibles", "Validación Combobox", parent=ventana)
self.focus_set()
def set_font(self, font):
self.entry.config(font=font)
self.listbox.config(font=font)
def set_state(self, estado):
if estado == "normal":
self.entry.config(state=estado)
self.listbox.config(state=estado)
elif estado == "readonly":
self.entry.config(state=estado)
self.listbox.config(state="normal")
elif estado == "disabled":
self.entry.config(state="readonly")
self.listbox.config(state="disabled")
def delete(self, inicio, fin):
self.entry.delete(inicio, fin)
class MainWindow:
def __init__(self):
self.root = tb.Window(themename="superhero")
self.root.geometry("300x300+0+0")
self.root.resizable(False, False)
self.root.focus()
self.root.grab_set()
style = tb.Style()
style.configure("primary.TButton", font = ("Calibri", 12, "bold"))
style.configure("danger.TButton", font = ("Calibri", 12, "bold"))
tb.Label(self.root, text="User", font = ("Calibri", 12, "bold")).pack(pady=10)
lista_usuarios =['Felix', 'Raquel', 'Marta', 'Gonzalo']
usuario = FilteredCombobox(self.root, height=5, width=20)
usuario.set_completion_list(lista_usuarios)
usuario.set_font(("Times Roman", 10))
usuario.pack(pady=15)
b_entrar= tb.Button(self.root, text="Enter", command=lambda: self.enter_app(), bootstyle="primary", style="primary.TButton")
b_entrar.pack(pady=10, ipadx=8)
b_salir= tb.Button(self.root, text="Exit", command=lambda: self.root.destroy(), bootstyle="danger", style="danger.TButton")
b_salir.pack(pady=10, ipadx=8)
self.root.mainloop()
def enter_app(self):
NextWindow()
class NextWindow(tb.Toplevel):
def __init__(self):
super().__init__()
self.geometry("300x300+0+0")
self.resizable(False, False)
self.focus()
self.grab_set()
frame = tb.Frame(self)
frame.pack(fill=tb.X)
Label = tb.Label(frame, text="Combo")
Label.grid(row=0, column=0, sticky=W)
combo = FilteredCombobox(frame, width=30, height=5)
combo.grid (row=0, column=1, sticky=W)
valores = ["primero", "segundo", "tercero", "cuarto", "quinto", "sexto", "septimo"]
combo.set_completion_list(valores)
def main():
MainWindow()
if __name__ == "__main__":
main()