Tkinter 如何捕捉这个特定的关键事件

Tkinter how to catch this particular key event

问题描述

我在 Tkinter 中有一个应用程序,它使用显示搜索结果的列表框。当我按 command + down 箭头键时,我将焦点从搜索字段放到列表框中的第一项。这正是我想要的行为方式,而只是 down 箭头。

但是,我已经通过 self.bind("<Down>", self.moveDown) 将向下箭头绑定到此列表框。我不明白为什么 command + down 有效,而 down (我确实将其绑定到)却不起作用。具体按下down箭头的结果如下 当按下 command + down 给出预期的结果时: 我怎样才能让 down 表现得像 command + down,为什么需要 command

代码片段

def matches(fieldValue, acListEntry):
    pattern = re.compile(re.escape(fieldValue) + '.*', re.IGNORECASE)
    return re.match(pattern, acListEntry)
root = Tk()
img = ImageTk.PhotoImage(Image.open('imgs/giphy.gif'))
panel = Label(root, image=img)
panel.grid(row=1, column=0)
entry = AutocompleteEntry(autocompleteList, panel, root, matchesFunction=matches)
entry.grid(row=0, column=0)
root.mainloop()

AutocompleteEntry 为:

class AutocompleteEntry(Tkinter.Entry):
    def __init__(self, autocompleteList, df, panel, rdi, *args, **kwargs):
        self.df = df
        self.product_row_lookup = {key:value for value, key in enumerate(autocompleteList)}
        temp = df.columns.insert(0, 'Product_omschrijving')
        temp = temp.insert(1, 'grams')
        self.result_list = pd.DataFrame(columns=temp)
        self.panel = panel
        self.rdi = rdi
        # self.bind('<Down>', self.handle_keyrelease)

        # Listbox length
        if 'listboxLength' in kwargs:
            self.listboxLength = kwargs['listboxLength']
            del kwargs['listboxLength']
        else:
            self.listboxLength = 8
        # Custom matches function
        if 'matchesFunction' in kwargs:
            self.matchesFunction = kwargs['matchesFunction']
            del kwargs['matchesFunction']
        else:
            def matches(fieldValue, acListEntry):
                pattern = re.compile('.*' + re.escape(fieldValue) + '.*', re.IGNORECASE)
                return re.match(pattern, acListEntry)
            self.matchesFunction = matches

        Entry.__init__(self, *args, **kwargs)
        self.focus()
        self.autocompleteList = autocompleteList
        self.var = self["textvariable"]
        if self.var == '':
            self.var = self["textvariable"] = StringVar()

        self.var.trace('w', self.changed)
        self.bind("<Right>", self.selection)
        self.bind("<Up>", self.moveUp)
        self.bind("<Down>", self.moveDown)
        self.bind("<Return>", self.selection)
        self.listboxUp = False
        self._digits = re.compile('\d')


    def changed(self, name, index, mode):
        if self.var.get() == '':
            if self.listboxUp:
                self.listbox.destroy()
                self.listboxUp = False
        else:
            words = self.comparison()
            if words:
                if not self.listboxUp:
                    self.listbox = Listbox(width=self["width"], height=self.listboxLength)
                    self.listbox.bind("<Button-1>", self.selection)
                    self.listbox.bind("<Right>", self.selection)
                    self.listbox.bind("<Down>", self.moveDown)
                    self.listbox.bind("<Tab>", self.selection)
                    self.listbox.place(x=self.winfo_x(), y=self.winfo_y() + self.winfo_height())
                    self.listboxUp = True

                self.listbox.delete(0, END)
                for w in words:
                    self.listbox.insert(END, w)
            else:
                if self.listboxUp:
                    self.listbox.destroy()
                    self.listboxUp = False
                else:
                    string = self.get()
                    if '.' in string:
                        write_to_file(self, string)

    def contains_digits(self, d):
        return bool(self._digits.search(d))


    def selection(self, event):
        if self.listboxUp:
            string = self.listbox.get(ACTIVE)
            self.var.set(string + ' ')
            self.listbox.destroy()
            self.listboxUp = False
            self.icursor(END)



    def moveDown(self, event):
        self.focus()
        if self.listboxUp:
            if self.listbox.curselection() == ():
                index = '0'
                print "ok"
            else:
                index = self.listbox.curselection()[0]
                print "blah"

            if index != END:
                self.listbox.selection_clear(first=index)
                print "noo"
                if index != '0':
                    index = str(int(index) + 1)

            self.listbox.see(index)  # Scroll!
            self.listbox.selection_set(first=index)
            self.listbox.activate(index)
        else:
            print "not up"


    def comparison(self):
        return [w for w in self.autocompleteList if self.matchesFunction(self.var.get(), w)]

command+down 和 down 应该产生相同的输出,除了 down 还会在条目上键入问号 ,这使得最后键入的字母是问号框。

这是因为按下命令后,您的计算机会检查选项菜单以查看是否有该键的快捷方式,如果没有,则不会执行任何操作。当 tkinter 将向下按钮注册为被按下时,因此事件被触发。

相比之下,在没有按下命令的情况下,Entry首先显示"down"没有的值,然后执行事件绑定,你可以做的是,在事件,删除条目的最后一个字母。您可以在您的活动中通过 self.delete(len(self.get())-1) 这样做。或者在 event 末尾添加 return 'break' 以防止输入。

不幸的是,很难理解你的真正问题,因为你发布了太多不相关的代码而没有足够的相关代码。在我看来,您想要完成的是让用户在条目具有焦点时按下向下或向上箭头,并使列表框中的选择向下或向上移动。此外,问题的一部分似乎是您在输入小部件中看到的字符是您按下或向上按时不想看到的。

如果那是问题所在,解决方法相当简单。您需要做的就是让您的绑定 return 字符串 "break" 以防止处理默认绑定。插入字符的是默认绑定。

这是一个例子。 运行例子,上下左右移动列表框的选择。我省略了与自动完成相关的所有代码,因此您可以专注于事件绑定的工作原理。

import Tkinter as tk

class Example(object):
    def __init__(self):

        self.root = tk.Tk()

        self.entry = tk.Entry(self.root)
        self.listbox = tk.Listbox(self.root, exportselection=False)
        for i in range(30):
            self.listbox.insert("end", "Item #%s" % i)

        self.entry.pack(side="top", fill="x")
        self.listbox.pack(side="top", fill="both", expand=True)


        self.entry.bind("<Down>", self.handle_updown)
        self.entry.bind("<Up>", self.handle_updown)

    def start(self):
        self.root.mainloop()

    def handle_updown(self, event):
        delta = -1 if event.keysym == "Up" else 1
        curselection = self.listbox.curselection()
        if len(curselection) == 0:
            index = 0
        else:
            index = max(int(curselection[0]) + delta, 0)

        self.listbox.selection_clear(0, "end")
        self.listbox.selection_set(index, index)

        return "break"

if __name__ == "__main__":
    Example().start()

有关触发事件时发生的情况的详尽解释,请参阅此答案:

同样,撇开自动完成要求不谈,我想出了一个解决方案,该解决方案使用 ListboxScrollbar 的标准可用命令和事件。 <<ListboxSelect>> 让您可以捕捉任何列表中选择的变化并对齐其他列表。此外,ScrollbarListbox 回调被定向到路由函数,该函数将内容传递给所有列表框。

# updownmultilistbox.py
# 7/24/2020
#
# incorporates vsb to propagate scrolling across lists
#

import tkinter as tk


class Example(object):
    def __init__(self):
        self.root = tk.Tk()
        self.listOfListboxes = []
        # self.active_lb = None
        self.vsb = tk.Scrollbar(orient='vertical', command=self.OnVsb)
        self.vsb.pack(side='right', fill='y')

        self.lb1 = tk.Listbox(self.root, exportselection=0,
                              selectmode= tk.SINGLE, yscrollcommand=self.vsb_set)
        self.lb2 = tk.Listbox(self.root, exportselection=0,
                              selectmode=tk.SINGLE, yscrollcommand=self.vsb_set)
        self.lb3 = tk.Listbox(self.root, exportselection=0,
                              selectmode=tk.SINGLE, yscrollcommand=self.vsb_set)

        self.listOfListboxes.append(self.lb1)
        self.listOfListboxes.append(self.lb2)
        self.listOfListboxes.append(self.lb3)

        for i in range(30):
            self.lb1.insert("end", "lb1 Item #%s" % i)
            self.lb2.insert("end", "lb2 Item #%s" % i)
            self.lb3.insert("end", "lb3 Item #%s" % i)

        self.lb1.pack(side="left", fill="both", expand=True)
        self.lb2.pack(side="left", fill="both", expand=True)
        self.lb3.pack(side="left", fill="both", expand=True)

        for lb in self.listOfListboxes:
            lb.bind('<<ListboxSelect>>', self.handle_select)

        for lb in self.listOfListboxes:
            lb.selection_set(0)
            lb.activate(0)
        self.listOfListboxes[0].focus_force()

    def start(self):
        self.root.title('updownmultilistbox')
        self.root.mainloop()

    def OnVsb(self, *args):
        for lb in self.listOfListboxes:
            lb.yview(*args)

    def vsb_set(self, *args):
        print ('vsb_set args: ', *args)
        self.vsb.set(*args)
        for lb in self.listOfListboxes:
            lb.yview_moveto(args[0])


    def handle_select(self, event):
        # set evey list to the same selection
        print ('select handler: ', event, event.widget.curselection())
        # self.active_lb = event.widget
        for lb in self.listOfListboxes:
            if lb != event.widget:
                lb.selection_clear(0, 'end')    # have to avoid this for the current widget
                lb.selection_set(event.widget.curselection())
                lb.activate(event.widget.curselection())
if __name__ == "__main__":
    Example().start()