一次按下多个键时自动完成失败

Autocomplete fails when multiple keys are pressed at once

我正在尝试实现用于选择字体的自动完成功能,其中必须选择除用户插入的文本之外的其余文本。

我已经成功编写了一个工作代码,但是当我同时按下两个键时失败了。当我同时按下两个键时,第一个匹配的值将插入到 Entry 小部件中,而无需选择其余字母。

How can I select the rest of the letters which are to be autocomplete text even if two keys are pressed simultaneously?

代码

import string
import tkinter as tk
from tkinter import font

class AutoComplete:
    def __init__(self):
        self.keys = []
        self.function_keys = ['Ctrl_L', 'Ctrl_R', 'Shift_L', 'Shift_R', 'Alt_L', 'Alt_R']

        self.root = tk.Tk()
        self.test_list = self.non_duplicate_fonts()

        self.entry_var = tk.StringVar()
        self.entry = tk.Entry(self.root, textvariable=self.entry_var)
        self.entry.pack()
        self.entry.focus_set()

        self.lis_var = tk.Variable(self.root, value=self.test_list)
        self.listbox = tk.Listbox(self.root, listvariable=self.lis_var)
        self.listbox.pack()

        self.entry.bind('<KeyPress>', self.key_pressed)
        self.entry.bind('<KeyRelease>', self.on_keyrelease)
        self.root.mainloop()

    def is_function_key_available(self):
        for key in self.keys:
            if key in self.function_keys:
                return True

        return False

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

        fonts = list(font.families())
        fonts.sort()
        fonts = fonts[26:]

        prev_family = ' '
        _fonts = []

        for family in fonts:
            if not family.startswith(prev_family):
                _fonts.append(family)
                prev_family = family

        return _fonts

    def on_keyrelease(self, event):
        if self.is_function_key_available():
            self.keys = []
            return

        elif event.keysym in string.printable:
            value = self.entry_var.get().strip().lower()
            lowered_list = [k.lower() for k in self.test_list]
            matched_value = [f for f in lowered_list if f.startswith(value)]

            if matched_value:
                index = lowered_list.index(matched_value[0])
                self.entry_var.set(self.test_list[index])
                self.entry.selection_range(len(value), 'end')

        self.keys = []

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

        if key not in self.keys:
            self.keys.append(key)


if __name__ == '__main__':
    AutoComplete()

进口

import string
from tkinter import *
from tkinter import font

设置小部件

    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

设置默认值

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.

按下按键时需要检查:

  • space转换为“”

  • 仅当该键可打印时才将每个按下的键存储到一个变量中

  • 如果 selection 存在于 Entry 小部件中,则删除 selected 文本并插入与Entry 小部件中的文本并将 CURSOR 向前移动一步。还通过增加 1 来存储起始索引,以便我们可以将其用于 select 剩余文本。

    如果 selection 不存在,则将起始索引增加 1 并重复剩余的索引。

     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'
    
  • 使用 bindtag

    绑定 Entry 小部件
      self.entry.bind('<KeyPress>', self.key_pressed)
    

当返回space键被按下时

  • 如果 光标 位置还没有到达 Entry 小部件的开头。

    • 如果存在 selection 则删除 selected 值并将光标索引设置为剩余文本的长度并将闪烁时间设置回默认值。

    • 如果 selection 不存在,则只需从 Entry 小部件中删除最后一个值并将其余值插入 Entry 小部件并将光标索引减 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'
      
  • 使用 bindtag

    绑定 Entry 小部件
      self.entry.bind('<BackSpace>', self.backspace)
    

当按下 Tab 键时

  • Select 条目 小部件中的所有文本

  • 将光标设置到 Entry 小部件的末尾并删除闪烁

  • listbox 中删除先前的 selection 并 select listbox[=182] 中的新值=]

      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'
    
  • 使用 bindtag

    绑定 Entry 小部件
      self.entry.bind('<Tab>', self.tab_completion)
    

当按下向上键时

  • Select 值高于 listbox 中的 selection,直到索引达到 0

  • 仅将 selected 值插入 条目 来自 listbox 和 select 的小部件条目 小部件

    中的所有文本
      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'
    
  • 使用 bindtag

    绑定 Entry 小部件
      self.entry.bind('<Up>', self.up_direction)
    

按下向下按钮时

  • Select值低于listboxselection直到索引达到总数列表框.

    中的值
  • 仅将 selected 值插入 条目 来自 listbox 和 select 的小部件条目 小部件

    中的所有文本
      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'
    
  • 使用 bindtag 绑定 Entry widget

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

Here, return 'break' forbids tkinter to execute its default bindings

当selection是通过点击列表框中的值

  • 列表框 中的 selected 值插入 条目 小部件

      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 条目 小部件中的所有文本

      def set_selection(self):
          '''Select all text in entry widget'''
    
          self.entry.select_range(0, 'end')
          self.entry.focus()
    
  • 使用 bindtag 绑定 ListBox 小部件 <>

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

自动完成功能说明

  • 条目 小部件中获取值

  • 从列表框列表中获取与在 条目 小部件

    中输入的文本相匹配的值
  • 如果 matched 非空则获取其第一个值并获取其索引并将该值插入 Entry 小部件,如果 cursor 索引等于第一个匹配值,则 select Entry 小部件中的所有文本 else select 从最近插入的值索引到 end 并将 listbox 滚动到第一个匹配值的索引

      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)
    

把所有东西放在一起

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()