# Author: Miguel Martinez Lopez
# Version: 0.3
import re
try:
from Tkinter import StringVar, Entry, Frame, Listbox
from ttk import Scrollbar
from Tkconstants import *
except ImportError:
from tkinter import StringVar, Entry, Frame, Listbox
from tkinter.ttk import Scrollbar
from tkinter.constants import *
def autoscroll(sbar, first, last):
"""Hide and show scrollbar as needed."""
first, last = float(first), float(last)
if first <= 0 and last >= 1:
sbar.grid_remove()
else:
sbar.grid()
sbar.set(first, last)
class Combobox_Autocomplete(Entry, object):
def __init__(self, master, list_of_items=None, autocomplete_function=None, listbox_width=None, listbox_height=7, ignorecase_match=False, startswith_match=True, vscrollbar=True, hscrollbar=True, **kwargs):
if hasattr(self, "autocomplete_function"):
if autocomplete_function is not None:
raise ValueError("Combobox_Autocomplete subclass has 'autocomplete_function' implemented")
else:
if autocomplete_function is not None:
self.autocomplete_function = autocomplete_function
else:
if list_of_items is None:
raise ValueError("If not guiven complete function, list_of_items can't be 'None'")
if ignorecase_match:
if startswith_match:
def matches_function(entry_data, item):
return item.startswith(entry_data)
else:
def matches_function(entry_data, item):
return item in entry_data
self.autocomplete_function = lambda entry_data: [item for item in self.list_of_items if matches_function(entry_data, item)]
else:
if startswith_match:
def matches_function(escaped_entry_data, item):
if re.match(escaped_entry_data, item, re.IGNORECASE):
return True
else:
return False
else:
def matches_function(escaped_entry_data, item):
if re.search(escaped_entry_data, item, re.IGNORECASE):
return True
else:
return False
def autocomplete_function(entry_data):
escaped_entry_data = re.escape(entry_data)
return [item for item in self.list_of_items if matches_function(escaped_entry_data, item)]
self.autocomplete_function = autocomplete_function
self.listbox_height = int(listbox_height)
self.listbox_width = listbox_width
self.list_of_items = list_of_items
self._use_vscrollbar = vscrollbar
self._use_hscrollbar = hscrollbar
kwargs.setdefault("background", "white")
if "textvariable" in kwargs:
self._entry_var = kwargs["textvariable"]
else:
self._entry_var = kwargs["textvariable"] = StringVar()
Entry.__init__(self, master, **kwargs)
self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)
self.listbox = None
self.bind("<Tab>", self._on_tab)
self.bind("<Up>", self._on_move_up)
self.bind("<Down>", self._on_move_down)
self.bind("<Return>", self._update_entry_from_listbox)
self.bind("<Escape>", lambda event: self.close_listbox())
def _on_tab(self, event):
self.open_listbox()
return "break"
def _on_change_entry_var(self, name, index, mode):
entry_data = self._entry_var.get()
if entry_data == '':
self.close_listbox()
self.focus()
else:
values = self.autocomplete_function(entry_data)
if values:
if self.listbox is None:
self._build_listbox(values)
else:
self.listbox.delete(0, END)
height = min(self.listbox_height, len(values))
self.listbox.configure(height=height)
for item in values:
self.listbox.insert(END, item)
else:
self.close_listbox()
self.focus()
def _build_listbox(self, values):
listbox_frame = Frame()
self.listbox = Listbox(listbox_frame, background="white", selectmode=SINGLE, activestyle="none", exportselection=False)
self.listbox.grid(row=0, column=0,sticky = N+E+W+S)
self.listbox.bind("<ButtonRelease-1>", self._update_entry_from_listbox)
self.listbox.bind("<Return>", self._update_entry_from_listbox)
self.listbox.bind("<Escape>", lambda event: self.close_listbox())
if self._use_vscrollbar:
vbar = Scrollbar(listbox_frame, orient=VERTICAL, command= self.listbox.yview)
vbar.grid(row=0, column=1, sticky=N+S)
self.listbox.configure(yscrollcommand= lambda f, l: autoscroll(vbar, f, l))
if self._use_hscrollbar:
hbar = Scrollbar(listbox_frame, orient=HORIZONTAL, command= self.listbox.xview)
hbar.grid(row=1, column=0, sticky=E+W)
self.listbox.configure(xscrollcommand= lambda f, l: autoscroll(hbar, f, l))
listbox_frame.grid_columnconfigure(0, weight= 1)
listbox_frame.grid_rowconfigure(0, weight= 1)
x = -self.cget("borderwidth") - self.cget("highlightthickness")
y = self.winfo_height()-self.cget("borderwidth") - self.cget("highlightthickness")
if self.listbox_width:
width = self.listbox_width
else:
width=self.winfo_width()
listbox_frame.place(in_=self, x=x, y=y, width=self.winfo_width())
height = min(self.listbox_height, len(values))
self.listbox.configure(height=height)
for item in values:
self.listbox.insert(END, item)
def open_listbox(self):
if self.listbox is not None: return
entry_data = self._entry_var.get()
if entry_data == '': return
values = self.autocomplete_function(entry_data)
if values:
self._build_listbox(values)
def close_listbox(self):
if self.listbox is not None:
self.listbox.master.destroy()
self.listbox = None
def get_value(self):
return self._entry_var.get()
def set_value(self, text, close_dialog=False):
if close_dialog:
self._entry_var.trace_vdelete("w", self._trace_id)
self._entry_var.set(text)
self.close_listbox()
self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)
else:
self._entry_var.set(text)
self.icursor(END)
self.xview_moveto(1.0)
def _update_entry_from_listbox(self, event):
if self.listbox is not None:
current_selection = self.listbox.curselection()
if current_selection:
self._entry_var.set(self.listbox.get(current_selection))
self.listbox.master.destroy()
self.listbox = None
self.icursor(END)
self.xview_moveto(1.0)
def _on_move_up(self, event):
if self.listbox is not None:
current_selection = self.listbox.curselection()
if len(current_selection)==0:
self.listbox.selection_set(0)
self.listbox.activate(0)
else:
index = int(current_selection[0])
self.listbox.selection_clear(index)
if index == 0:
index = END
else:
index -= 1
self.listbox.see(index)
self.listbox.selection_set(first=index)
self.listbox.activate(index)
def _on_move_down(self, event):
if self.listbox is not None:
current_selection = self.listbox.curselection()
if len(current_selection)==0:
self.listbox.selection_set(0)
self.listbox.activate(0)
else:
index = int(current_selection[0])
self.listbox.selection_clear(index)
if index == self.listbox.size() - 1:
index = 0
else:
index +=1
self.listbox.see(index)
self.listbox.selection_set(index)
self.listbox.activate(index)
if __name__ == '__main__':
try:
from Tkinter import Tk
except ImportError:
from tkinter import Tk
list_of_items = ["Cordell Cannata", "Lacey Naples", "Zachery Manigault", "Regan Brunt", "Mario Hilgefort", "Austin Phong", "Moises Saum", "Willy Neill", "Rosendo Sokoloff", "Salley Christenberry", "Toby Schneller", "Angel Buchwald", "Nestor Criger", "Arie Jozwiak", "Nita Montelongo", "Clemencia Okane", "Alison Scaggs", "Von Petrella", "Glennie Gurley", "Jamar Callender", "Titus Wenrich", "Chadwick Liedtke", "Sharlene Yochum", "Leonida Mutchler", "Duane Pickett", "Morton Brackins", "Ervin Trundy", "Antony Orwig", "Audrea Yutzy", "Michal Hepp", "Annelle Hoadley", "Hank Wyman", "Mika Fernandez", "Elisa Legendre", "Sade Nicolson", "Jessie Yi", "Forrest Mooneyhan", "Alvin Widell", "Lizette Ruppe", "Marguerita Pilarski", "Merna Argento", "Jess Daquila", "Breann Bevans", "Melvin Guidry", "Jacelyn Vanleer", "Jerome Riendeau", "Iraida Nyquist", "Micah Glantz", "Dorene Waldrip", "Fidel Garey", "Vertie Deady", "Rosalinda Odegaard", "Chong Hayner", "Candida Palazzolo", "Bennie Faison", "Nova Bunkley", "Francis Buckwalter", "Georgianne Espinal", "Karleen Dockins", "Hertha Lucus", "Ike Alberty", "Deangelo Revelle", "Juli Gallup", "Wendie Eisner", "Khalilah Travers", "Rex Outman", "Anabel King", "Lorelei Tardiff", "Pablo Berkey", "Mariel Tutino", "Leigh Marciano", "Ok Nadeau", "Zachary Antrim", "Chun Matthew", "Golden Keniston", "Anthony Johson", "Rossana Ahlstrom", "Amado Schluter", "Delila Lovelady", "Josef Belle", "Leif Negrete", "Alec Doss", "Darryl Stryker", "Michael Cagley", "Sabina Alejo", "Delana Mewborn", "Aurelio Crouch", "Ashlie Shulman", "Danielle Conlan", "Randal Donnell", "Rheba Anzalone", "Lilian Truax", "Weston Quarterman", "Britt Brunt", "Leonie Corbett", "Monika Gamet", "Ingeborg Bello", "Angelique Zhang", "Santiago Thibeau", "Eliseo Helmuth"]
root = Tk()
root.geometry("300x200")
combobox_autocomplete = Combobox_Autocomplete(root, list_of_items, highlightthickness=1)
combobox_autocomplete.pack()
combobox_autocomplete.focus()
root.mainloop()
Diff to Previous Revision
--- revision 5 2017-04-02 23:02:38
+++ revision 6 2017-04-02 23:21:27
@@ -1,5 +1,5 @@
# Author: Miguel Martinez Lopez
-# Version: 0.2
+# Version: 0.3
import re
@@ -24,7 +24,7 @@
class Combobox_Autocomplete(Entry, object):
- def __init__(self, master, list_of_items=None, autocomplete_function=None, listbox_length=7, ignorecase_match=False, startswith_match=True, vscrollbar=True, hscrollbar=True, **kwargs):
+ def __init__(self, master, list_of_items=None, autocomplete_function=None, listbox_width=None, listbox_height=7, ignorecase_match=False, startswith_match=True, vscrollbar=True, hscrollbar=True, **kwargs):
if hasattr(self, "autocomplete_function"):
if autocomplete_function is not None:
raise ValueError("Combobox_Autocomplete subclass has 'autocomplete_function' implemented")
@@ -64,7 +64,9 @@
self.autocomplete_function = autocomplete_function
- self.listbox_length = int(listbox_length)
+ self.listbox_height = int(listbox_height)
+ self.listbox_width = listbox_width
+
self.list_of_items = list_of_items
self._use_vscrollbar = vscrollbar
@@ -83,12 +85,17 @@
self.listbox = None
+ self.bind("<Tab>", self._on_tab)
self.bind("<Up>", self._on_move_up)
self.bind("<Down>", self._on_move_down)
self.bind("<Return>", self._update_entry_from_listbox)
self.bind("<Escape>", lambda event: self.close_listbox())
+ def _on_tab(self, event):
+ self.open_listbox()
+ return "break"
+
def _on_change_entry_var(self, name, index, mode):
entry_data = self._entry_var.get()
@@ -104,7 +111,7 @@
else:
self.listbox.delete(0, END)
- height = min(self.listbox_length, len(values))
+ height = min(self.listbox_height, len(values))
self.listbox.configure(height=height)
for item in values:
@@ -142,9 +149,14 @@
x = -self.cget("borderwidth") - self.cget("highlightthickness")
y = self.winfo_height()-self.cget("borderwidth") - self.cget("highlightthickness")
+ if self.listbox_width:
+ width = self.listbox_width
+ else:
+ width=self.winfo_width()
+
listbox_frame.place(in_=self, x=x, y=y, width=self.winfo_width())
- height = min(self.listbox_length, len(values))
+ height = min(self.listbox_height, len(values))
self.listbox.configure(height=height)
for item in values: