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

Posted

技术标签:

【中文标题】在 python tkinter 中每次连续单击后都会触发双击事件【英文标题】:Double-click event fires after every successive click in python tkinter 【发布时间】:2019-10-29 11:05:18 【问题描述】:

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

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

有没有办法“重置”双击,这样我的程序就会认为下一次单击是单击,无论我之前做了什么?

我尝试使用单击事件坐标来获取“双击”条目,但是此在第三次而不是第四次单击时触发,这是不希望的行为。我还尝试绑定三次单击以阻止第二次双击,但是如果我单击超过 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 交互,而无需等待双击冷却时间过去。另外,我不想通过(相对于当前视图)单击来双击新条目。

【问题讨论】:

【参考方案1】:

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

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 函数绑定到单击。 这有效,因为: * 如果用户双击,则单击将dclick设置为True,这意味着第二次单击计为双击。 * 如果用户随后单击,则 dclick 标志设置回 False,这意味着它被视为单次单击。 * 如果用户等待延迟,第一次点击会重置标志,因为它算作一次。

它未经测试,但我希望它有所帮助。

【讨论】:

【参考方案2】:

我会创建一个新类来记住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 可能是该类的方法。在这种情况下应该更改函数名,因为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()

【讨论】:

@ReblochonMasque 好一个。将其添加到答案中。【参考方案3】:

以下内容的灵感来自@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()

【讨论】:

【参考方案4】:

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

虽然我可以通过对绑定到双击的函数进行以下编辑来应用抑制错误双击的想法(或者更好:让它调用单击功能):

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)

这是可行的,因为在这里,可以通过检查是否选择了某些内容来检测错误的双击,如果没有,则表示之前没有发生过单击。

【讨论】:

以上是关于在 python tkinter 中每次连续单击后都会触发双击事件的主要内容,如果未能解决你的问题,请参考以下文章

如何在不单击tkinter python的情况下读取单选按钮值

按钮点击后,Python tkinter在框架中显示文本

如何在没有按钮单击python tkinter的情况下进入另一个页面

如何在 Python Tkinter 中创建一个按钮以将整数变量增加 1 并显示该变量?

如何在 Tkinter 中选择颜色?

在python tkinter中调用事件时,变量不更新的标签