How to implement Autocomplete functionality in tkinter?

Question:

I am trying to make a autocomplete functionality for selecting fonts where the rest of the text must be selected apart from the text inserted by the user.

How to implement Autocomplete functionality in tkinter ?

Asked By: Ghanteyyy

||

Answers:

Imports

import string
from tkinter import *
from tkinter import font

Setting up the widgets

    class AutoComplete:
        def __init__(self):
            self.root = Tk()
            self.root.title('AutoComplete')

            self.ent_var = StringVar()
            self.entry = Entry(self.root, textvariable=self.ent_var, width=50)
            self.entry.pack()

            self.list_var = Variable(self.entry)
            self.listbox = Listbox(self.root, listvariable=self.list_var, width=50)
            self.listbox.pack()

            self.all_fonts = list(font.families())
            self.all_fonts.sort()
            self.all_fonts = self.all_fonts[26:]
            self.non_duplicates_fonts()
            self.lower_case = [f.lower() for f in self.all_fonts]

            self.list_var.set(self.all_fonts)
            self.entry.focus()

            self.root.resizable(0, 0)
            self.root.mainloop()

        def non_duplicates_fonts(self):
            '''Filter fonts starting with same name'''

            prev_family = ' '
            font_families = []

            for family in self.all_fonts:
                if not family.startswith(prev_family):
                    font_families.append(family)
                    prev_family = family

            self.all_fonts = font_families

Setting default values

def default(self):
    '''Set default values in entry widget and listbox'''

    self.listbox.selection_set(3)
    self.ent_var.set('Arial')

    self.entry.config(insertofftime=1000000, insertontime=0)
    self.set_selection()

To make a auto-complete functionality in tkinter, you need to bind
your text widget with bindtags <KeyPress> to detect if any keys
are pressed down.

When keys are pressed down then you need to check:

  • Convert space to " "

  • Store each pressed key to a variable only if that key is printable

  • If selection is present in Entry widget then remove the selected text and insert the keys pressed along with the text present in Entry widget and shift the CURSOR to one step forward. Also store the starting index by increasing by 1 so that we can use it to select the remaining text.

    If selection is absent then increase the starting index by 1 and repeat the remaining.

     def key_pressed(self, event=None):
         key = event.keysym
    
         if key == 'space':
             key = ' '
    
         if key in string.printable:
             if self.entry.selection_present():
                 self.sel = self.entry.index('sel.first') + 1
                 self.entry.delete('sel.first', 'sel.last')
    
             else:
                 self.sel = self.entry.index('insert') + 1
    
             value = self.ent_var.get() + key
             self.ent_var.set(value)
             self.ent_index += 1
    
             self.entry.icursor(self.ent_index)
             self.auto_complete()  # Explained below
    
         return 'break'
    
  • Bind Entry widget with bindtag <KeyPress>

      self.entry.bind('<KeyPress>', self.key_pressed)
    

When backspace key is pressed

  • If cursor position has not reached to the beginning of the Entry widget.

    • If selection is present then remove the selected value and set the cursor index to the length of the remaining text and set blinking time back to default.

    • If selection is not present then just remove the last value from the Entry widget and insert rest of to the Entry widget and decrease the cursor index by 1.

        def backspace(self, event=None):
            value = self.entry.get()[:-1]
            self.ent_var.set(value)
      
            if self.ent_index != 0:
                if self.entry.selection_present():
                    self.entry.delete('sel.first', 'sel.last')
                    self.ent_index = len(self.ent_var.get())
      
                    if self.entry['insertofftime'] == 1000000:  # Restore time of blinking to default
                        self.entry.config(insertofftime=300, insertontime=600)
      
                else:
                    self.ent_index -= 1
      
            return 'break'
      
  • Bind Entry widget with bindtag <BackSpace>

      self.entry.bind('<BackSpace>', self.backspace)
    

When tab key is pressed

  • Select all text in Entry widget

  • Set cursor to the end of the Entry widget and remove blinking

  • Remove previous selection from the listbox and select the new value in listbox

      def tab_completion(self, event=None):
          '''Select all text in entry widget of matched one.
             Also select the same value in listbox'''
    
          value = self.ent_var.get()
    
          self.entry.selection_range(0, 'end')
          self.entry.icursor('end')
    
          index = self.all_fonts.index(value)
          self.listbox.selection_clear(0, 'end')
          self.listbox.selection_set(index)
    
          self.entry.config(insertofftime=1000000, insertontime=0)  # Removing blinking of cursor.
          return 'break'
    
  • Bind Entry widget with bindtag <Tab>

      self.entry.bind('<Tab>', self.tab_completion)
    

When UP key is pressed

  • Select value above the selection in listbox until the index reaches to 0

  • Insert just selected value to entry widget from listbox and select all text in entry widget

      def up_direction(self, event=None):
          '''Move selection in upwards direction in listbox'''
    
          index = self.listbox.curselection()[0]
    
          if index > 0:
              index -= 1
    
              self.listbox.selection_clear(0, 'end')
              self.listbox.selection_set(index)
              self.listbox.see(index)
    
              self.ent_var.set(self.all_fonts[index])
              self.entry.selection_range(0, 'end')
    
          return 'break'
    
  • Bind Entry widget with bindtag <Up>

      self.entry.bind('<Up>', self.up_direction)
    

When DOWN button is pressed

  • Select value below the selection in listbox until the index reaches to the total number of values in listbox.

  • Insert just selected value to entry widget from listbox and select all text in entry widget

      def down_direction(self, event=None):
          '''Move selection in downwards direction in listbox'''
    
          index = self.listbox.curselection()[0]
    
          if index < len(self.all_fonts) - 1:
              index += 1
    
              self.listbox.selection_clear(0, 'end')
              self.listbox.selection_set(index)
              self.listbox.see(index)
    
              self.ent_var.set(self.all_fonts[index])
              self.entry.selection_range(0, 'end')
    
          return 'break'
    
  • Bind Entry widget with bindtag <Down>

      self.entry.bind('<Down>', self.down_direction)
    

Here, return ‘break’ forbids tkinter to execute its default bindings

When selection is made by clicking value in listbox

  • Insert the selected value from listbox in entry widget

      def button_click(self, event=None):
          '''When selection is made by clicking'''
    
          index = self.listbox.curselection()[0]
    
          self.ent_var.set(self.all_fonts[index])
          self.root.after(10, self.set_selection)
    
  • Select all text in entry widget

      def set_selection(self):
          '''Select all text in entry widget'''
    
          self.entry.select_range(0, 'end')
          self.entry.focus()
    
  • Bind ListBox widget with bindtag <<ListboxSelect>>

      self.listbox.bind('<<ListboxSelect>>', self.button_click)
    

Auto-Complete function explanation

  • Grab value from the Entry widget

  • Get matched value from the list of list-box with respect to the text entered in Entry widget

  • If matched is non-empty then grab its first value and get its index and insert that value in Entry widget and if the cursor index is equal to the first matched value then select all text in Entry widget else select from the recent inserted value index to the end and scroll the listbox to the index of the first matched value

      def auto_complete(self):
          value = self.ent_var.get().strip().lower()
          matched = [f for f in self.lower_case if f.startswith(value)]
    
          if matched:
              matched = matched[0]
              index = self.lower_case.index(matched)
    
              self.ent_var.set(self.all_fonts[index])
    
              if self.entry.index('insert') == len(matched):
                  self.entry.selection_range(0, 'end')
    
              else:
                  self.entry.selection_range(self.sel, 'end')
    
              self.listbox.see(index)
    

Putting everything together

import string
from tkinter import *
from tkinter import font


class AutoComplete:
    def __init__(self):
        self.ent_index = 0

        self.root = Tk()
        self.root.title('AutoComplete')

        self.ent_var = StringVar()
        self.entry = Entry(self.root, textvariable=self.ent_var, width=50)
        self.entry.pack()

        self.list_var = Variable(self.entry)
        self.listbox = Listbox(self.root, listvariable=self.list_var, exportselection=False, activestyle='none', selectmode=SINGLE, width=50)
        self.listbox.pack()

        self.all_fonts = list(font.families())
        self.all_fonts.sort()
        self.all_fonts = self.all_fonts[26:]
        self.non_duplicates_fonts()
        self.lower_case = [f.lower() for f in self.all_fonts]

        self.list_var.set(self.all_fonts)
        self.entry.focus()

        self.entry.bind('<Up>', self.up_direction)
        self.entry.bind('<Tab>', self.tab_completion)
        self.entry.bind('<BackSpace>', self.backspace)
        self.entry.bind('<Down>', self.down_direction)
        self.entry.bind('<KeyPress>', self.key_pressed)
        self.listbox.bind('<<ListboxSelect>>', self.button_click)

        self.default()

        self.root.resizable(0, 0)
        self.root.mainloop()

    def non_duplicates_fonts(self):
        '''Filter fonts starting with same name'''

        prev_family = ' '
        font_families = []

        for family in self.all_fonts:
            if not family.startswith(prev_family):
                font_families.append(family)
                prev_family = family

        self.all_fonts = font_families

    def default(self):
        '''Set default values in entry widget and listbox'''

        self.listbox.selection_set(3)
        self.ent_var.set('Arial')

        self.entry.config(insertofftime=1000000, insertontime=0)
        self.set_selection()

    def key_pressed(self, event=None):
        key = event.keysym

        if key == 'space':
            key = ' '

        if key in string.printable:
            if self.entry.selection_present():
                self.sel = self.entry.index('sel.first') + 1
                self.entry.delete('sel.first', 'sel.last')

            else:
                self.sel = self.entry.index('insert') + 1

            value = self.ent_var.get() + key
            self.ent_var.set(value)
            self.ent_index += 1

            self.entry.icursor(self.ent_index)
            self.auto_complete()

        return 'break'

    def backspace(self, event=None):
        value = self.entry.get()[:-1]
        self.ent_var.set(value)

        if self.ent_index != 0:
            if self.entry.selection_present():
                self.entry.delete('sel.first', 'sel.last')
                self.ent_index = len(self.ent_var.get())

                if self.entry['insertofftime'] == 1000000:  # Restore time of blinking to default
                    self.entry.config(insertofftime=300, insertontime=600)

            else:
                self.ent_index -= 1

        return 'break'

    def tab_completion(self, event=None):
        '''Select all text in entry widget of matched one.
           Also select the same value in listbox'''

        value = self.ent_var.get()

        self.entry.selection_range(0, 'end')
        self.entry.icursor('end')

        index = self.all_fonts.index(value)
        self.listbox.selection_clear(0, 'end')
        self.listbox.selection_set(index)

        self.entry.config(insertofftime=1000000, insertontime=0)
        return 'break'

    def auto_complete(self):
        value = self.ent_var.get().strip().lower()
        matched = [f for f in self.lower_case if f.startswith(value)]

        if matched:
            matched = matched[0]
            index = self.lower_case.index(matched)

            self.ent_var.set(self.all_fonts[index])

            if self.entry.index('insert') == len(matched):
                self.entry.selection_range(0, 'end')
                self.listbox.selection_clear(0, 'end')
                self.listbox.selection_set(index)

            else:
                self.entry.selection_range(self.sel, 'end')

            self.listbox.see(index)

    def down_direction(self, event=None):
        '''Move selection in downwards direction in listbox'''

        index = self.listbox.curselection()[0]

        if index < len(self.all_fonts) - 1:
            index += 1

            self.listbox.selection_clear(0, 'end')
            self.listbox.selection_set(index)
            self.listbox.see(index)

            self.ent_var.set(self.all_fonts[index])
            self.entry.selection_range(0, 'end')

        return 'break'

    def up_direction(self, event=None):
        '''Move selection in upwards direction in listbox'''

        index = self.listbox.curselection()[0]

        if index > 0:
            index -= 1

            self.listbox.selection_clear(0, 'end')
            self.listbox.selection_set(index)
            self.listbox.see(index)

            self.ent_var.set(self.all_fonts[index])
            self.entry.selection_range(0, 'end')

        return 'break'

    def button_click(self, event=None):
        '''When selection is made by clicking'''

        index = self.listbox.curselection()[0]

        self.ent_var.set(self.all_fonts[index])
        self.root.after(10, self.set_selection)

    def set_selection(self):
        '''Select all text in entry widget'''

        self.entry.select_range(0, 'end')
        self.entry.focus()


if __name__ == '__main__':
    AutoComplete()
Answered By: Ghanteyyy
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.