class AutocompleteCombobox(ttk.Combobox): """ A Combobox widget with autocomplete functionality. Features: - Real-time filtering as user types - Dropdown shows matching items only - Supports case-insensitive matching - Maintains original data list - Customizable match function - Handles arrow keys for navigation - Supports both string and display-friendly values """ def __init__( self, master=None, completevalues: List[Any] = None, case_sensitive: bool = False, match_function: Optional[Callable] = None, **kwargs ): """ Initialize the autocomplete combobox. Args: master: Parent widget completevalues: List of all possible values case_sensitive: Whether matching should be case-sensitive match_function: Custom function to determine matches (takes value and search text) **kwargs: Additional arguments for ttk.Combobox """ # Store the complete list of all possible values self._completevalues = completevalues or [] self._case_sensitive = case_sensitive self._match_function = match_function or self._default_match_function # Initialize the combobox super().__init__(master, **kwargs) # Track the current value to avoid recursion self._current_value = "" # Bind events self.bind('<KeyRelease>', self._on_keyrelease) self.bind('<FocusOut>', self._on_focusout) self.bind('<Return>', self._on_return) self.bind('<Tab>', self._on_tab) # Configure the dropdown listbox self._configure_listbox() # Set initial values self._update_autocomplete() def _configure_listbox(self): """Configure the dropdown listbox for better interaction.""" # This method is called after the widget is created to access the listbox # We need to wait until the dropdown is created def configure_listbox_callback(): try: # Get the listbox from the combobox's internal widget listbox = self.tk.call('ttk::combobox::PopdownWindow', self, 'listbox') if listbox: # Bind events to the listbox self._listbox = listbox self.tk.call('bind', listbox, '<ButtonRelease-1>', lambda e: self._on_listbox_select()) except tk.TclError: # Listbox not yet created, try again later self.after(100, configure_listbox_callback) self.after(100, configure_listbox_callback) def _default_match_function(self, item: Any, search_text: str) -> bool: """ Default matching function - checks if search text is contained in the item. Args: item: The item to check search_text: The text to search for Returns: True if the item matches the search text """ if not search_text: return True item_str = str(item) if self._case_sensitive: return search_text in item_str else: return search_text.lower() in item_str.lower() def _update_autocomplete(self): """Update the dropdown values based on current input.""" current_text = self.get() # Don't update if nothing changed if current_text == self._current_value: return self._current_value = current_text # Filter the values filtered_values = [ item for item in self._completevalues if self._match_function(item, current_text) ] # Update the dropdown list if filtered_values: self['values'] = filtered_values # Show dropdown if there are matches and we have input if current_text: self.event_generate('<Down>') self.event_generate('<Down>') # Sometimes needed to properly show dropdown else: self['values'] = [] # Hide dropdown if no matches try: self.tk.call('ttk::combobox::PopdownWindow', self, 'hide') except tk.TclError: pass def _on_keyrelease(self, event): """Handle key release events to update autocomplete.""" # Ignore navigation keys if event.keysym in ('Up', 'Down', 'Left', 'Right', 'Home', 'End', 'Page_Up', 'Page_Down', 'Return', 'Tab'): return # Update the autocomplete list self._update_autocomplete() def _on_focusout(self, event): """Handle focus out event.""" # Validate current value when focus is lost current_text = self.get() if current_text and current_text not in self['values']: # Optionally clear invalid input or keep as is # self.set('') # Uncomment to clear invalid input pass def _on_return(self, event): """Handle Return key press.""" current_text = self.get() if current_text and current_text not in self['values'] and self['values']: # If current text is not in values but we have matches, select the first match self.set(self['values'][0]) self.event_generate('<Return>') def _on_tab(self, event): """Handle Tab key press.""" current_text = self.get() if current_text and current_text not in self['values'] and self['values']: # Auto-complete with the first match when tab is pressed self.set(self['values'][0]) self.icursor(tk.END) return 'break' # Prevent default tab behavior return None def _on_listbox_select(self): """Handle selection from dropdown listbox.""" # This is called when user clicks on an item in the dropdown # The value is automatically set by the combobox self.after(10, self._validate_and_update) def _validate_and_update(self): """Validate the selected value and update autocomplete.""" current_text = self.get() if current_text and current_text in self['values']: # Valid selection, update autocomplete for next time self._update_autocomplete() def set_completevalues(self, values: List[Any]): """ Update the complete list of possible values. Args: values: New list of all possible values """ self._completevalues = values self._update_autocomplete() def get_completevalues(self) -> List[Any]: """ Get the complete list of possible values. Returns: List of all possible values """ return self._completevalues.copy() def set_match_function(self, match_function: Callable): """ Set a custom matching function. Args: match_function: Function that takes (item, search_text) and returns bool """ self._match_function = match_function self._update_autocomplete()
import tkinter as tk from tkinter import ttk import re from typing import List, Callable, Optional, Any autocomplete combobox tkinter
# Example usage and demonstration class AutocompleteDemo: """Demonstration class for the autocomplete combobox.""" def __init__(self): self.root = tk.Tk() self.root.title("Autocomplete Combobox Demo") self.root.geometry("600x500") # Sample data self.countries = [ "United States", "United Kingdom", "United Arab Emirates", "Canada", "Mexico", "Brazil", "Argentina", "Germany", "France", "Spain", "Italy", "Netherlands", "Belgium", "Switzerland", "Austria", "Sweden", "Norway", "Denmark", "Finland", "Iceland", "Ireland", "Portugal", "Greece", "Turkey", "Russia", "China", "Japan", "South Korea", "India", "Australia", "New Zealand", "South Africa", "Egypt", "Nigeria", "Kenya", "Morocco" ] self.programming_languages = [ "Python", "Java", "JavaScript", "TypeScript", "C++", "C#", "Ruby", "PHP", "Swift", "Kotlin", "Go", "Rust", "Scala", "Perl", "Haskell", "Lua", "Dart", "R", "MATLAB", "Julia" ] self.create_widgets() def create_widgets(self): """Create and arrange all widgets.""" # Title title_label = tk.Label( self.root, text="Autocomplete Combobox Examples", font=("Arial", 16, "bold") ) title_label.pack(pady=10) # Basic example basic_frame = tk.LabelFrame(self.root, text="Basic Autocomplete", padx=10, pady=10) basic_frame.pack(fill="x", padx=20, pady=10) tk.Label(basic_frame, text="Select a country:").pack(anchor="w") self.basic_combobox = AutocompleteCombobox( basic_frame, completevalues=self.countries, width=30 ) self.basic_combobox.pack(fill="x", pady=5) self.basic_combobox.set("") # Case-sensitive example case_frame = tk.LabelFrame(self.root, text="Case-Sensitive Matching", padx=10, pady=10) case_frame.pack(fill="x", padx=20, pady=10) tk.Label(case_frame, text="Select a programming language (case-sensitive):").pack(anchor="w") self.case_combobox = AutocompleteCombobox( case_frame, completevalues=self.programming_languages, case_sensitive=True, width=30 ) self.case_combobox.pack(fill="x", pady=5) self.case_combobox.set("") # Advanced example advanced_frame = tk.LabelFrame(self.root, text="Advanced Autocomplete (with recent items)", padx=10, pady=10) advanced_frame.pack(fill="x", padx=20, pady=10) tk.Label(advanced_frame, text="Select a country (tracks recent selections):").pack(anchor="w") self.advanced_combobox = AdvancedAutocompleteCombobox( advanced_frame, completevalues=self.countries, max_items=15, enable_recent=True, max_recent=5, width=30 ) self.advanced_combobox.pack(fill="x", pady=5) self.advanced_combobox.set("") # Custom match function example custom_frame = tk.LabelFrame(self.root, text="Custom Matching (Starts With)", padx=10, pady=10) custom_frame.pack(fill="x", padx=20, pady=10) tk.Label(custom_frame, text="Select a country (matches from beginning):").pack(anchor="w") def starts_with_match(item, search_text): if not search_text: return True if self.custom_case.get(): return str(item).startswith(search_text) else: return str(item).lower().startswith(search_text.lower()) self.custom_combobox = AutocompleteCombobox( custom_frame, completevalues=self.countries, match_function=starts_with_match, width=30 ) self.custom_combobox.pack(fill="x", pady=5) self.custom_combobox.set("") self.custom_case = tk.BooleanVar(value=False) case_check = tk.Checkbutton( custom_frame, text="Case-sensitive", variable=self.custom_case, command=lambda: self.custom_combobox.set_match_function(starts_with_match) ) case_check.pack(anchor="w", pady=5) # Status and info info_frame = tk.Frame(self.root) info_frame.pack(fill="x", padx=20, pady=10) self.status_label = tk.Label(info_frame, text="Select an item to see details", font=("Arial", 10)) self.status_label.pack() # Buttons button_frame = tk.Frame(self.root) button_frame.pack(pady=10) tk.Button( button_frame, text="Get Selected Values", command=self.show_selected_values, padx=10 ).pack(side="left", padx=5) tk.Button( button_frame, text="Clear All", command=self.clear_all, padx=10 ).pack(side="left", padx=5) tk.Button( button_frame, text="Update Country List", command=self.update_country_list, padx=10 ).pack(side="left", padx=5) # Bind selection events self.basic_combobox.bind('<<ComboboxSelected>>', self.on_selection) self.case_combobox.bind('<<ComboboxSelected>>', self.on_selection) self.advanced_combobox.bind('<<ComboboxSelected>>', self.on_selection) self.custom_combobox.bind('<<ComboboxSelected>>', self.on_selection) def on_selection(self, event): """Handle selection events.""" widget = event.widget value = widget.get() self.status_label.config(text=f"Selected: {value}") def show_selected_values(self): """Show all selected values in a message box.""" values = { "Basic": self.basic_combobox.get() or "(not selected)", "Case-Sensitive": self.case_combobox.get() or "(not selected)", "Advanced": self.advanced_combobox.get() or "(not selected)", "Custom": self.custom_combobox.get() or "(not selected)" } message = "\n".join([f"{key}: {value}" for key, value in values.items()]) # Simple message box alternative msg_window = tk.Toplevel(self.root) msg_window.title("Selected Values") msg_window.geometry("400x200") msg_window.transient(self.root) msg_window.grab_set() tk.Label(msg_window, text="Selected Values:", font=("Arial", 12, "bold")).pack(pady=10) tk.Label(msg_window, text=message, justify="left").pack(pady=10) tk.Button(msg_window, text="Close", command=msg_window.destroy).pack(pady=10) def clear_all(self): """Clear all comboboxes.""" self.basic_combobox.set("") self.case_combobox.set("") self.advanced_combobox.set("") self.custom_combobox.set("") self.status_label.config(text="All fields cleared") def update_country_list(self): """Dynamically update the country list.""" new_countries = ["Atlantis", "El Dorado", "Shangri-La", "Lemuria", "Mu"] self.countries.extend(new_countries) # Update all comboboxes that use the country list self.basic_combobox.set_completevalues(self.countries) self.advanced_combobox.set_completevalues(self.countries) self.custom_combobox.set_completevalues(self.countries) self.status_label.config(text=f"Added {len(new_countries)} new countries!") def run(self): """Run the demo application.""" self.root.mainloop() class AutocompleteCombobox(ttk