在 python tkinter 中每次连续单击后都会触发双击事件

Double-click event fires after every successive click in python tkinter

我正在使用 tkinter 列表框编写类似资源管理器的应用程序。双击,我想进入选定的文件夹,所以我清除列表并输入文件夹内容。

当我双击后直接点击时,仍然认为是新的双击。因此,没有执行单击,这意味着没有选择列表框条目,并且在我实际再次双击之前发生了变化。

有没有办法 "reset" 双击,这样无论我之前做了什么,我的程序都会将下一次单击视为单击?

我尝试使用点击事件坐标来获取 "double-clicked" 条目,但是这个 在第三次而不是第四次点击时触发,这是不希望的行为。我也试过绑定三次点击来屏蔽第二次双击,但是点击超过3次程序就没有响应,延迟一段时间才反应过来。

import tkinter as tk
import random


def fill_box(event):
    """Clear and refresh listbox"""
    try:
        listbox.get(listbox.curselection()[0])
        selection = True
    except IndexError:
        selection = False
    print("Event:", event, "Selection:", selection)
    listbox.delete(0, tk.END)
    for _ in range(10):
        listbox.insert(tk.END, random.randint(0, 1000))


root = tk.Tk()
listbox = tk.Listbox(root)
for _ in range(10):
    listbox.insert(tk.END, random.randint(0, 1000))
listbox.bind("<Double-Button-1>", fill_box)
# listbox.bind("<Triple-Button-1>", lambda x: 1)  # Triple click
listbox.pack()
root.mainloop()

我的期望是在我双击一个条目后,我可以立即再次与 GUI 交互,而不必等待双击冷却时间过去。另外,我不想通过单击(相对于当前视图)双击新条目。

解决此问题的最佳方法是使用一个全局变量来存储点击状态。 将其放在开头:

dclick = False
def sclick(e):
    global dclick
    dclick = True #Set double click flag to True. If the delay passes, the flag will be reset on the next click

然后,将 fill_box 函数替换为:

    """Clear and refresh listbox"""
    global dclick
    if not dclick: #if clicked before the delay passed
        sclick(event) #treat like a single click
        return #Do nothing else
    else: #if this is an actual double click
        dclick = False #the next double is a single
    try:
        listbox.get(listbox.curselection()[0])
        selection = True
    except IndexError:
        selection = False
    print("Event:", event, "Selection:", selection)
    listbox.delete(0, tk.END)
    for _ in range(10):
        listbox.insert(tk.END, random.randint(0, 1000))

然后,将sclick功能绑定到单击。 这是有效的,因为: * 如果用户 double-clicks,dclick 被 single-click 设置为 True,这意味着第二次点击算作双击。 * 如果用户然后 single-clicks,dclick 标志被设置回 False,这意味着它被视为单个。 * 如果用户等待延迟结束,第一次点击会重置标志,因为它算作一次点击。

未经测试,但希望对您有所帮助。

我会通过创建一个新的 class 来记住 curselection 并拦截快速点击来处理它:

import tkinter as tk
import random


class MyListbox(tk.Listbox):
    def __init__(self, parent):
        super().__init__(parent)
        self.clicked = None


def fill_box(event):
    """Clear and refresh listbox"""
    try:
        listbox.get(listbox.curselection()[0])
        selection = True
    except IndexError:
        selection = False
        activate()                               # intercept rapid click
        return
    print("Event:", event, "Selection:", selection)
    listbox.clicked = listbox.curselection()[0]  # remember the curselection
    listbox.delete(0, tk.END)
    for _ in range(10):
        listbox.insert(tk.END, random.randint(0, 1000))

def activate():
    listbox.selection_set(listbox.clicked)


root = tk.Tk()

listbox = MyListbox(root)
for _ in range(10):
    listbox.insert(tk.END, random.randint(0, 1000))
listbox.bind("<Double-Button-1>", fill_box)
listbox.pack()

root.mainloop()

如@Reblochon Masque 所述,activate 可能是 class 的方法。在这种情况下,函数名称应该更改,因为 listbox 有自己的 activate 方法:

class MyListbox(tk.Listbox):
    def __init__(self, parent):
        super().__init__(parent)
        self.clicked = None

    def activate_clicked(self):
        self.selection_set(listbox.clicked)

并且可以称为 listbox.activate_clicked() 而不是 activate()

以下灵感来自@VladimirShkaberda 的回答。

FastClickListboxtk.Listbox 的子类,它抽象了处理快速连续双击的逻辑,没有延迟。这允许用户专注于双击触发的所需操作,而不用担心实现细节。

import tkinter as tk
import random


class FastClickListbox(tk.Listbox):
    """a listbox that allows for rapid fire double clicks
    by keeping track of the index last selected, and substituting
    it when the next click happens before the new list is populated

    remembers curselection and intercepts rapid successive double clicks
    """

    def _activate(self, ndx):
        if ndx >= 0:
            self.ACTIVE = ndx
            self.activate(ndx)
            return True
        else:
            self.selection_set(self.ACTIVE)
            self.activate(self.ACTIVE)
            return False

    def _curselection(self):
        ndxs = self.curselection()
        return ndxs if len(ndxs) > 0 else (-1,)

    def is_ready(self):
        """returns True if ready, False otherwise
        """
        return self._activate(listbox._curselection()[0])


# vastly simplified logic on the user side
def clear_and_refresh(dummy_event):
    if listbox.is_ready():
        listbox.delete(0, tk.END)
        for _ in range(random.randint(1, 11)):
            listbox.insert(tk.END, random.randint(0, 1000))


root = tk.Tk()
listbox = FastClickListbox(root)

for _ in range(random.randint(1, 11)):
    listbox.insert(tk.END, random.randint(0, 1000))

listbox.bind("<Double-Button-1>", clear_and_refresh)
listbox.pack()

root.mainloop()

在尝试实现@Vadim Shkaberda 和@Reblochon Masque 的解决方案时,我 运行 遇到了一个问题,这些解决方案对于示例来说完美无缺,但显然我把它做得太小了,无法捕捉到所有的东西,因为这些解决方案在我的项目中引入了一些新的边缘案例,例如当我以编程方式刷新列表时。

虽然我可以通过对绑定到 [=17= 的函数进行以下编辑来应用抑制错误 double-click 的想法(或更好:让它调用 single-click 功能) ]:

def double_clicked(event):
    """Check if double-click was genuine, if not, perform single-click function."""
    try:
        current = self.current_val()
    except KeyError:  # False-positive Double-click
        # Simulate single click funktion by marking the currently hovered item
        index = self.listbox.index("@{},{}".format(event.x, event.y))
        self.listbox.select_set(index)
        return
    # If this is reached, a genuine Double-click happened
    functionality()

listbox.bind("<Double-Button-1>", double_clicked)

这是有效的,因为在这里,可以通过检查是否选择了某些东西来检测错误的 double-click,如果没有,则之前没有发生过 single-click。