создать пользовательскую панель поиска tkinter

Я видел много дискуссий о строке поиска, но так и не нашел чего-то действительно удобного. Я сделал собственный searchBar (может быть, мы добавим его в tkinter, если он сработает!).

Если вы попробуете этот код (Combobox), он позволяет списку перекрывать другой виджет (поведение по умолчанию, я не волшебник!), но вы не можете отображать список И одновременно удерживать фокус на тексте, чтобы помочь Пользователь:

import tkinter as tk
from tkinter import ttk

app=tk.Tk()
app_FRAME=tk.Frame(app,relief=tk.GROOVE)    
app_FRAME.pack()


def clicked_on_arrow():
    print("see the list")

def var_changed():
    print("var changed")

_list = ["a","b","c"]
_var = tk.StringVar()
_var.trace("w", lambda name, index, mode, x=_var: var_changed())

_COMBOBOX = ttk.Combobox(app_FRAME, postcommand=clicked_on_arrow(), values=_list, textvariable=_var)
_COMBOBOX.pack()
random_BUTTON = tk.Button(app_FRAME, text="the list can overlap over me !")
random_BUTTON.pack(fill=tk.BOTH, expand=True)

app.mainloop()

Этот код (более сложный и пользовательский) не может перекрываться (потому что я не совсем уверен, как реализовать его для создания пользовательского виджета, не зная корневого фрейма), но он действительно помогает пользователю, визуализируя список в режиме реального времени:

# -*- coding: utf-8 -*-
"""
Created on Thu Apr  9 17:25:59 2020

@author: aymeric LAUGEL
"""

import tkinter as tk

class Search_Bar:

    def create_new_list(self,string_given, string_list):
        new_list = []
        for string in string_list:
            if(len(string_given) <=len(string)):
                if(string.find(string_given) != -1):
                    new_list.append(string)
        return new_list      

    def update_display_list(self):
        if(self.listboxExist):
            self.m_LISTBOX.delete(0,tk.END)
            for string in self.m_list_to_display: self.m_LISTBOX.insert('end', string)
        else:
            print("listbox should exist...")


    def maybe_update_display_LISTBOX(self):
        possible_new_list_to_display = self.create_new_list(self.m_text_entry_object.get(), self.m_list)
        print()
        print("#############")
        print("LISTBOX exist:",self.listboxExist)
        print("var: |"+self.m_text_entry_object.get()+"|")
        print("old list:",self.m_list_to_display)
        print("new list:",possible_new_list_to_display)


        if(len(possible_new_list_to_display) == 0):
            print("nothing to display")
            if(self.listboxExist):
                self.delete_LISTBOX()
            else:
                print("La listbox n'existe déjà plus")
            return

        if possible_new_list_to_display == self.m_list_to_display:
            print("list are the same, no need to update")
            if(self.listboxExist):
                pass
            else:
                self.create_LISTBOX()
                print("Listbox should already exist cause length is more than 0...")
            return        

        if(len(possible_new_list_to_display) == len(self.m_list_to_display)):
            print("just need to update the values, not the length")
            self.m_list_to_display = possible_new_list_to_display
            if(self.listboxExist):
                self.update_display_list()
            else:
                print("Listbox should have already been created.")
                self.create_LISTBOX()
            return
        if(len(possible_new_list_to_display) >= self.m_max_element_to_display and 
           len(self.m_list_to_display) >= self.m_max_element_to_display):
            print("not the same len but we are gonna display the max number of row anyway")
            self.m_list_to_display = possible_new_list_to_display
            if(self.listboxExist):
                self.update_display_list()
            else:
                if not(self.itemInListboxSelected):
                    print("Listbox should have already been created....")
                    self.create_LISTBOX()

            return
        print("we have to change the length of the LISTBOX display (and the values)")
        self.m_list_to_display = possible_new_list_to_display
        if(self.listboxExist):
            self.delete_LISTBOX()
        self.create_LISTBOX()


    def func_called_when_text_change(self,event):
        print("the text changed")
        self.maybe_update_display_LISTBOX()

    def focus_in_ENTRY(self, event):
        print("focus in ENTRY")
        self.focusIn_ENTRY = True
        self.maybe_create_LISTBOX()

    def focus_out_ENTRY(self, event):
        print("focus out ENTRY")
        self.focusIn_ENTRY = False
        self.maybe_delete_LISTBOX()

    def cursor_in_ENTRY(self, event):
        print("cursor in ENTRY")
        self.cursorIn_ENTRY = True

    def cursor_out_ENTRY(self, event):
        print("cursor out ENTRY")
        self.cursorIn_ENTRY = False


    def focus_in_LISTBOX(self, event):
        print("focus in LISTBOX")
        self.focusIn_LISTBOX = True

    def focus_out_LISTBOX(self, event):
        print("focus out LISTBOX")
        self.focusIn_LISTBOX = False
        self.maybe_delete_LISTBOX()

    def cursor_in_LISTBOX(self, event):
        print("cursor in LISTBOX")
        self.cursorIn_LISTBOX = True

    def cursor_out_LISTBOX(self, event):
        print("cursor out LISTBOX")
        self.cursorIn_LISTBOX = False


    def maybe_create_LISTBOX(self):
        if(self.focusIn_ENTRY == True and self.listboxExist == False):
            self.create_LISTBOX()

    def maybe_delete_LISTBOX(self):
        if(#self.focusIn_ENTRY == False and 
           self.listboxExist == True and 
           self.cursorIn_LISTBOX == False and 
           self.cursorIn_ENTRY == False):
            self.delete_LISTBOX()

    def item_selected(self,event):
        print("an item have been selected from the listbox")
        self.itemInListboxSelected = True
        selected_item_tuple_id = self.m_LISTBOX.curselection()
        if(selected_item_tuple_id != ()):
            string = str(self.m_LISTBOX.get(self.m_LISTBOX.curselection()))
            self.m_text_entry_object.set(string)
        self.m_list_to_display = []
        self.delete_LISTBOX()
        self.itemInListboxSelected = False

    def create_LISTBOX(self):
        print("we create the listbox")
        if(self.listboxExist):
            print("Listbox should not be here")
            return
        if(self.m_LISTBOX != None):
            print("THE LIST SHOULD HAVE BEEN ABSENT !!!")
            return

        if(len(self.m_list_to_display) == 0):
            print("We shouldn't try to display an empty listbox...")
            return
        if(self.m_max_element_to_display > len(self.m_list_to_display)):
            nbr_of_row_to_display = len(self.m_list_to_display)
        else:
            nbr_of_row_to_display = self.m_max_element_to_display

        self.m_LISTBOX = tk.Listbox(self.m_FRAME, height=nbr_of_row_to_display)
        self.m_LISTBOX.pack()
        self.m_LISTBOX.bind("<FocusIn>", self.focus_in_LISTBOX)
        self.m_LISTBOX.bind("<FocusOut>", self.focus_out_LISTBOX)
        self.m_LISTBOX.bind("<Enter>", self.cursor_in_LISTBOX)
        self.m_LISTBOX.bind("<Leave>", self.cursor_out_LISTBOX)
        self.m_LISTBOX.bind('<<ListboxSelect>>', self.item_selected)
        self.listboxExist = True
        self.update_display_list()



    def delete_LISTBOX(self):
        print("we delete the listbox")
        if not(self.listboxExist):
            print("Listbox should be here")
            return
        if(self.m_LISTBOX == None):
            print("THE LIST SHOULD HAVE BEEN PRESENT !!!")
            return
        self.m_LISTBOX.destroy()
        del self.m_LISTBOX
        self.m_LISTBOX = None
        self.list_to_display = []
        self.listboxExist = False



    def __init__(self, actual_FRAME, _list, max_element_to_display=4):
        self.itemInListboxSelected = False
        self.focusIn_ENTRY = False
        self.cursorIn_ENTRY = False
        self.cursorIn_LISTBOX = False
        self.listboxExist = False
        self.m_list = _list
        if(max_element_to_display > len(_list)):
            print("vous ne pouvez pas display plus d'éléments qu'il y en a dans la list...")
            self.m_max_len_to_display = len(_list)
        self.m_max_element_to_display = max_element_to_display
        self.m_list_to_display = self.m_list
        self.m_FRAME = actual_FRAME
        self.m_text_entry_object = tk.StringVar()
        self.m_text_entry_object.trace("w", lambda name, index, mode, x=self.m_text_entry_object: self.func_called_when_text_change(x))
        self.m_ENTRY = tk.Entry(self.m_FRAME, textvariable=self.m_text_entry_object)
        self.m_ENTRY.pack(side = tk.TOP)

        self.m_ENTRY.bind("<FocusIn>", self.focus_in_ENTRY)
        self.m_ENTRY.bind("<FocusOut>", self.focus_out_ENTRY)
        self.m_ENTRY.bind("<Enter>", self.cursor_in_ENTRY)
        self.m_ENTRY.bind("<Leave>", self.cursor_out_ENTRY)

        self.m_LISTBOX = None
        #self.create_LISTBOX()
        #self.m_FRAME.pack_propagate(0)
        #self.delete_LISTBOX()






if __name__ == "__main__":

    app=tk.Tk()
    app.geometry('500x300')
    _list = ["fruit de la passion", "pomme","poire","pêche","abricot","pamplemousse","orange","raisin","cassis"]
    search_bar_FRAME=tk.Frame(app,relief=tk.GROOVE, borderwidth=5)   
    search_bar_FRAME.pack(side=tk.TOP,expand=True)
    SEARCH_BAR = Search_Bar(search_bar_FRAME, _list, max_element_to_display=5)


    _list_2 = ["amour","gloire","pouvoir de l'instant présent","beauté","guerre","action"]
    search_bar_2_FRAME=tk.Frame(app,relief=tk.GROOVE, borderwidth=5)   
    search_bar_2_FRAME.pack(side=tk.TOP)
    SEARCH_BAR_2 = Search_Bar(search_bar_2_FRAME, _list_2, max_element_to_display=3)

    other_FRAME=tk.Frame(app,relief=tk.GROOVE, borderwidth=5)    
    other_FRAME.pack()
    _random_Button = tk.Button(other_FRAME, text="This search bar can't overlap over me...")
    _random_Button.pack(side=tk.LEFT)
    app.mainloop()

Мой вопрос:

Как перекрыть другие виджеты списком, например списком со списком, но при этом помочь пользователю, когда он что-то набирает, показывая список?

Надеюсь, я был понятен, вы можете запустить 2 кода по отдельности, чтобы увидеть плюсы и минусы.

Любые советы, примеры или помощь приветствуются! :)

Может быть, как настроить Combobox, потому что это почти так!

Другие темы, говорящие о чем-то, что может помочь:


person Community    schedule 11.04.2020    source источник


Ответы (2)


Я вижу, что пост увидели более 2 тысяч человек, но никто ничего не сказал. Поэтому я предполагаю, что люди заинтересованы в этом виджете, но не знают, как это сделать. Вот новая версия (которой действительно не помешала бы очистка), но она работает в 90% необходимых случаев.

Если вы используете этот виджет, поставьте лайк, чтобы я увидел, помог ли я людям или всем наплевать ^^

# -*- coding: utf-8 -*-
"""
Created on Thu May 21 12:04:08 2020

@author: aymeric LAUGEL
"""

from tkinter import StringVar,Entry,Listbox

class Searchbar:
    def __init__(self, app, frame, _list=[],when_item_selected_func=lambda x,y:None,state="normal",width=20):
        
        self.app = app
        self.itemInListboxSelected = False
        self.element_list = _list
        self.max_element_to_display = 5
        self.list_to_display = self.element_list
        self.var = StringVar()
        self.var.trace("w", lambda name, index, mode, x=self.var: self.on_text_change(x))
        self.entry = Entry(frame, textvariable=self.var, highlightthickness=2,state=state,width=width)
        self.when_item_selected_func = when_item_selected_func
        self.listbox = None
        self.old_value = ""
        if state == "readonly":
            self.readOnly = True
        else:
            self.readOnly = False
    
    def on_app_click(self,widget):
        if widget == self.entry and self.entry.focus_get():
            self.create_listbox()
        else:
            self.delete_listbox()
    
    
    def on_text_change(self,event):
        if self.entry.focus_get():
            self.maybe_update_display_listbox()
    
    def maybe_create_listbox(self):
        if(self.listbox != None):
            return
        new_list = self.get_new_list(self.var.get(),self.element_list)
        if(len(new_list)<=2):
            return
        
        self.create_listbox()
    
    def get_new_list(self,string_given, string_list):
        if self.readOnly: return self.element_list
        new_list = []
        string_given_lower = string_given.lower()
        for string in string_list:
            string_lower = string.lower()
            if(len(string_given) <=len(string)):
                if(string_lower.find(string_given_lower) != -1):
                    new_list.append(string)
        return new_list   
    
    def item_selected(self,event):
        self.itemInListboxSelected = True
        selected_item_tuple_id = self.listbox.curselection()
        if(selected_item_tuple_id == ()):
            return
        idx = selected_item_tuple_id[0]
        string = str(self.listbox.get(self.listbox.curselection()))
        self.var.set(string)
        self.delete_listbox()
        self.when_item_selected_func(idx,string)
    
    def update_display_list(self):
        if(self.listbox == None):
            #print("error: listbox should exist before calling this function...")
            return
        self.listbox.delete(0,"end")
        for string in self.list_to_display: self.listbox.insert('end', string)
    
    def maybe_update_display_listbox(self):
        new_value = self.var.get()
        if self.old_value == new_value: return
        else: self.old_value = new_value
        
        possible_new_list_to_display = self.get_new_list(new_value, self.element_list)
        
        if(len(possible_new_list_to_display) == 0):
            #print("nothing to display")
            self.m_list_to_display = possible_new_list_to_display
            if(self.listbox != None):
                self.delete_listbox()
            else:
                pass
                #print("La listbox n'existe déjà plus")
            return
        
        elif possible_new_list_to_display == self.list_to_display:
            #print("list are the same, no need to update")
            if(self.listbox != None):
                pass
                #print("There is already a list so it's ok")
            else:
                self.create_listbox()
                #print("error: listbox should already exist cause length is more than 0...")
            return        
        
        elif(len(possible_new_list_to_display) == len(self.list_to_display)):
            #print("just need to update the values, not the length")
            self.list_to_display = possible_new_list_to_display
            if(self.listbox != None):
                self.update_display_list()
            else:
                #print("error: listbox should have already been created.")
                self.create_listbox()
            return
        elif(len(possible_new_list_to_display) >= self.max_element_to_display and 
           len(self.list_to_display) >= self.max_element_to_display):
            #print("not the same len but we are gonna display the max number of row anyway")
            self.list_to_display = possible_new_list_to_display
            if(self.listbox != None):
                self.update_display_list()
            else:
                if not(self.itemInListboxSelected):
                    print("error: listbox should have already been created....")
                    self.create_listbox()
                
            return
        #print("we have to change the length of the LISTBOX display (and the values)")
        self.list_to_display = possible_new_list_to_display
        if(self.listbox  != None):
            self.delete_listbox()
        self.create_listbox()
    
    def set_value(self, value):
        try:
            self.var.set(value)
            self.delete_listbox()
        except Exception as e:
            print("SearchBar.py l159")
            print(value)
            print(e)
    
    def create_listbox(self):
        #print("we create the listbox")
        if(self.listbox != None):
            #print("error: list should have been absent")
            return
        
        new_list = self.get_new_list(self.var.get(),self.element_list)
        
        if(len(new_list) == 0):
            #print("kind of error: We shouldn't try to display an empty listbox")
            return
        
        self.list_to_display = new_list
        
        if(self.max_element_to_display > len(self.list_to_display)):
            nbr_of_row_to_display = len(self.list_to_display)
        else:
            nbr_of_row_to_display = self.max_element_to_display
        width=self.entry.winfo_width()
        x=self.entry.winfo_rootx()-self.app.winfo_rootx() # frame !!!
        y=self.entry.winfo_rooty()-self.app.winfo_rooty()+self.entry.winfo_height() # frame !!!
        self.listbox = Listbox(self.app, width=width, height=nbr_of_row_to_display)
        self.listbox.place(x=x, y=y, width=width)
        self.listbox.bind('<<ListboxSelect>>', self.item_selected)
        #self.app.update()
        self.update_display_list()
        #print(self.element_list)
        #print(self.list_to_display)
        
    
    def delete_listbox(self):
        if(self.listbox == None):
            #print("error: list should have been present")
            return
        self.listbox.unbind('<<ListboxSelect>>')
        self.listbox.delete(0,"end")
        self.listbox.destroy()
        del self.listbox
        self.listbox = None
        self.list_to_display = []
        self.itemInListboxSelected = False
    
if __name__ == "__main__":
    from tkinter import Tk,Scrollbar,Canvas,Frame,Text
    def when_item_chosen_in_listbox(idx, string):
            print("voici l'indice:",idx)
            print("voici la valeur:",string)
    
    sb_dict = {}
    
    app = Tk()
    app.geometry('500x300')
    
    scrollbar = Scrollbar(app)
    scrollbar.pack( side = "right", fill = "y" )
    
    #app_frame=Frame(app,relief="groove", borderwidth=5, yscrollcommand = scrollbar.set)   
    #app_frame.pack(fill="both", expand=True)
    canvas = Canvas(app, yscrollcommand = scrollbar.set)
    canvas.pack(fill="both", expand=True)
    scrollbar.config( command = canvas.yview )
    
    _list = ["fruit de la passion", "pomme","poire","pêche","abricot","pamplemousse","orange","raisin","cassis"]
    sb1_frame = Frame(canvas, relief="groove", borderwidth=5)   
    sb1_frame.pack(side="top",expand=True)
    app.update()
    sb_dict["1"] = Searchbar(app, sb1_frame, _list,when_item_chosen_in_listbox)
    sb_dict["1"].entry.pack()
    
    _list_2 = ["amour","gloire","pouvoir de l'instant présent","beauté","guerre","action"]
    sb2_frame = Frame(canvas, relief="groove", borderwidth=5)   
    sb2_frame.pack(side="top")
    sb_dict["2"] = Searchbar(app, sb2_frame,state="readonly")
    sb_dict["2"].when_item_selected_func = when_item_chosen_in_listbox
    sb_dict["2"].element_list =_list_2
    sb_dict["2"].entry.pack()
    Text(app, height = 10).pack(side="bottom")
    
    def on_app_click(event):
        print("app click")
        [sb.on_app_click(event.widget) for sb in sb_dict.values()]
            
    app.bind("<Button-1>", on_app_click)
    """
    while True:
        sb = Searchbar(app, sb1_frame, _list,when_item_chosen_in_listbox)
        del sb
    """
    app.mainloop()
person Utopion    schedule 08.06.2021

Я сделал простую панель поиска, используя виджеты Entry и Listbox tkinter. Он также показывает наиболее подходящее значение текста, введенного в Entry Widget.

Я также добавил возможность настроить виджет ввода и списка в соответствии с вашими потребностями.

Посмотрите, поможет ли это вам

import tkinter as tk


def exampleFuncForAutoComplete(a, b):
    print(a)
    print(b)
    return ["a", "l", "4", "agasfasf", "afasfauy678", "858afas"]


def exampleFunctionToSearchBarValueChanged(value):
    print(value)


class SearchBar(tk.Frame):
    def __init__(self, parent, allItems, widthInText=30, autoCompleteFunction=None, valuesToDisplay=4):
        """
        :param parent: The Parent Object
        :param allItems: this specifies the Items From Which the search happens
        :param widthInText: The width of search bar in text units but not pixels.
        :param autoCompleteFunction: if you want to specify your own function for autocomplete. Default function will be
               used when set to None
        :param valuesToDisplay: The No Of Values to Display at a time
        """
        tk.Frame.__init__(self, parent)
        self.width = widthInText
        self.allItems = allItems
        self.autoCompleteFunction = autoCompleteFunction
        self.valuesToDisplay = valuesToDisplay
        if len(self.allItems) > self.valuesToDisplay:
            self.displayValues = self.allItems[:self.valuesToDisplay]
        else:
            self.displayValues = self.allItems
        self.searchBarOptionSelectedFunc = None
        self.searchVar = tk.StringVar()
        self.searchBar = tk.Entry(self, textvariable=self.searchVar, width=self.width, font=("Arial", 20),
                                  selectbackground="#d9d9d9", selectforeground="#000000")
        self.listBox = tk.Listbox(self, width=self.width, height=len(self.displayValues), font=("Arial", 20),
                                  activestyle='none', selectbackground="#e8e8e8", selectforeground="#000000",
                                  borderwidth=2)

        self.searchBar.pack(side=tk.TOP, fill=tk.Y)
        self.updateListBox(self.displayValues)

        self.searchBar.bind("<KeyRelease>", self.keyPressOnSearchBar)
        self.listBox.bind("<Return>", self.returnPressedOnListBox)
        self.searchBar.bind("<Tab>", self.tabPressedOnEntry)
        self.listBox.bind("<Tab>", self.tabPressedOnListBox)
        self.listBox.bind("<<ListboxSelect>>", self.optionSelectedInListBox)

    def updateListBox(self, items):
        self.listBox.delete(0, tk.END)
        for item in items:
            self.listBox.insert(tk.END, item)

    def matchString(self, text1):
        if self.autoCompleteFunction is None:
            # s = time.time()
            allStrings = self.allItems
            text1 = [i for i in text1.lower()]
            sameMatches = {}
            for j in allStrings:
                sameMatches[j] = 0
            for idx, word in enumerate(allStrings):

                letters = [i for i in word]
                for letter in text1:
                    for letter2 in letters:
                        if letter == letter2.lower():
                            sameMatches[allStrings[idx]] += 1

            marklist = sorted(sameMatches.items(), key=lambda x: x[1], reverse=True)
            allafgag = []
            for sortedS in marklist:
                allafgag.append(sortedS[0])
            # e = time.time()
            return allafgag
        else:
            functionSorted = self.autoCompleteFunction

            return functionSorted(text1, self.allItems)

    def returnPressedOnListBox(self, e=None):
        index = self.listBox.curselection()
        self.listBox.pack_forget()
        if len(index) > 0:
            index = index[0]
            self.searchVar.set(self.displayValues[index])
            if self.searchBarOptionSelectedFunc is not None:
                self.searchBarOptionSelectedFunc(self.displayValues[index])

    def tabPressedOnEntry(self, e=None):
        if self.listBox.winfo_ismapped():
            self.listBox.select_set(0)

    def optionSelectedInListBox(self, e=None):
        index = self.listBox.curselection()
        if len(index) > 0:
            index = index[0]
            self.searchVar.set(self.displayValues[index])
            if self.searchBarOptionSelectedFunc is not None:
                self.searchBarOptionSelectedFunc(self.displayValues[index])

    def tabPressedOnListBox(self, e=None):
        if self.listBox.winfo_ismapped():
            index = self.listBox.curselection()
            if len(index) > 0:
                index = index[0]
                self.searchVar.set(self.displayValues[index])
                if self.searchBarOptionSelectedFunc is not None:
                    self.searchBarOptionSelectedFunc(self.displayValues[index])
            else:
                self.searchVar.set(self.displayValues[0])
                if self.searchBarOptionSelectedFunc is not None:
                    self.searchBarOptionSelectedFunc(self.displayValues[0])
            self.listBox.pack_forget()

    def keyPressOnSearchBar(self, e=None):
        if e.keycode == 40: # Down Arrow presses on search Bar
            self.listBox.focus_set()
            self.listBox.select_set(0)
            self.searchVar.set(self.displayValues[0])
            if self.searchBarOptionSelectedFunc is not None:
                self.searchBarOptionSelectedFunc(self.displayValues[0])
        elif e.keycode == 13: # Enter Pressed on search Bar
            self.listBox.pack_forget()
            self.searchVar.set(self.displayValues[0])
            if self.searchBarOptionSelectedFunc is not None:
                self.searchBarOptionSelectedFunc(self.displayValues[0])
        else:
            searchBarText = self.searchVar.get()
            if searchBarText == "":
                self.listBox.pack_forget()
            else:
                dataInOrder = self.matchString(searchBarText)
                dataInOrder = dataInOrder[:self.valuesToDisplay]
                self.displayValues = dataInOrder
                self.updateListBox(dataInOrder)
                if not self.listBox.winfo_ismapped():
                    self.listBox.pack(side=tk.BOTTOM)

    def bind_Function_To_SearchBar_Option_Selected(self, func):
        self.searchBarOptionSelectedFunc = func

    def configureListBox(self, **options):
        self.listBox.configure(options)

    def configureSearchBar(self, **options):
        self.searchBar.configure(options)

    def getValue(self):
        return self.searchVar.get()


if __name__ == "__main__":
    root = tk.Tk()
    width, height = 1000, 800
    root.geometry(f"{width}x{height}")
    searchBar1 = SearchBar(root, ["Hello", "Morning", "Good", "Bye", "Camera", "Monitor", "Android"])
    searchBar1.pack()
    root.mainloop()
person Parv Jain    schedule 07.07.2021