Tkinter Entry部件中的撤销和重做?

5
有没有办法在Tkinter的Entry小部件中添加撤销和重做功能,或者我必须使用单行Text小部件来实现此类功能?
如果是后者,则在配置Text小部件以充当Entry小部件时应遵循哪些提示?
可能需要调整的一些功能包括捕获Return KeyPress,将Tab键按下转换为请求更改焦点,并从剪贴板粘贴的文本中删除换行符。
4个回答

3

请查看Tkinter自定义输入框。我已经添加了剪切、复制、粘贴上下文菜单以及撤销和重做功能。

# -*- coding: utf-8 -*-
from tkinter import *


class CEntry(Entry):
    def __init__(self, parent, *args, **kwargs):
        Entry.__init__(self, parent, *args, **kwargs)

        self.changes = [""]
        self.steps = int()

        self.context_menu = Menu(self, tearoff=0)
        self.context_menu.add_command(label="Cut")
        self.context_menu.add_command(label="Copy")
        self.context_menu.add_command(label="Paste")

        self.bind("<Button-3>", self.popup)

        self.bind("<Control-z>", self.undo)
        self.bind("<Control-y>", self.redo)

        self.bind("<Key>", self.add_changes)

    def popup(self, event):
        self.context_menu.post(event.x_root, event.y_root)
        self.context_menu.entryconfigure("Cut", command=lambda: self.event_generate("<<Cut>>"))
        self.context_menu.entryconfigure("Copy", command=lambda: self.event_generate("<<Copy>>"))
        self.context_menu.entryconfigure("Paste", command=lambda: self.event_generate("<<Paste>>"))

    def undo(self, event=None):
        if self.steps != 0:
            self.steps -= 1
            self.delete(0, END)
            self.insert(END, self.changes[self.steps])

    def redo(self, event=None):
        if self.steps < len(self.changes):
            self.delete(0, END)
            self.insert(END, self.changes[self.steps])
            self.steps += 1

    def add_changes(self, event=None):
        if self.get() != self.changes[-1]:
            self.changes.append(self.get())
            self.steps += 1

1
这应该是最佳答案。在这种情况下使用线程有点过度设计了... - scmg

2
免责声明:这些只是我在实施它时想到的想法。
class History(object):

    def __init__(self):
        self.l = ['']
        self.i = 0

    def next(self):
        if self.i == len(self.l):
            return None
        self.i += 1
        return self.l[self.i]

    def prev(self):
        if self.i == 0:
            return None
        self.i -= 1
        return self.l[self.i]

    def add(self, s):
        del self.l[self.i+1:]
        self.l.append(s)
        self.i += 1

    def current(self):
        return self.l[self.i]

运行一个线程,每X秒(0.5?)保存条目的状态:

history = History()
...
history.add(stringval.get())

您还可以设置事件,以保存条目的状态,例如按下“Return”键的压力。
prev = history.prev()
if prev is not None:
    stringvar.set(prev)

或者

next = history.next()
if next is not None:
    stringvar.set(next)

请注意根据需要设置锁定。


6
在这里使用线程是完全不必要的。小部件可以在更改时轻松地通知您。 - Bryan Oakley
@Bryan Oakley:不是很确切,这只是解决问题的另一种方法,其中之一。 - mg.
@MG:好计划:捕获每个小部件的更改,将总值存储在历史记录列表中,撤消时将历史指针向后移动一个级别并获取其值。重做时,将历史指针向前移动一个级别并获取其值。在存储用户更改时,每次用户更改后清除重做历史记录。我不喜欢线程监视-我需要等价于on-value-change事件(或可能是trace_var())的事件,以便我可以跟踪确切的更改。我发布了一个关于此类事件是否存在的问题:https://dev59.com/wlHTa4cB1Zd3GeqPU9ba - Malcolm
@Malcom:我知道这不是一个好的解决方案,但是一开始我并不想追踪每个变量的更改,只是一个想法,因为在事件循环中你可以设置超时。顺便说一下,这只是一个关于如何实现“History”跟踪的示例,最有趣的部分。看看对“History”类的更改,扩展“list”不是一个好主意。 - mg.
@MG:你的思维过程对我帮助很大,谢谢。 - Malcolm

2

关于使用此方法进行撤销/重做的更新:

我正在创建一个GUI,其中包含许多帧,每个帧至少包含十个或更多的“entry”小部件。我使用了History类并为每个输入字段创建了一个历史对象。我能够像这里一样将所有输入小部件的值存储在列表中。我使用附加到每个输入小部件的“trace”方法调用History类的“add”函数并存储每个更改。通过这种方式,我能够在不运行任何线程的情况下完成它。但是,使用此方法的最大缺点是,我们不能使用多个撤销/重做。

问题: 当我跟踪每个输入小部件的每个更改并将其添加到列表中时,它还会“跟踪”发生在撤销/重做时的更改,这意味着我们无法再向后退一步。一旦您执行撤销操作,它就是一个将被跟踪的更改,因此“撤销”值将在最后添加到列表中。因此,这不是正确的方法。

解决方案: 进行此操作的完美方法是为每个输入小部件创建两个堆栈。一个用于“撤销”,一个用于“重做”。每当输入发生更改时,请将该值推入撤销堆栈。当用户按下撤销时,请从撤销堆栈中弹出最后存储的值,并重要地将其推入“重做”堆栈。因此,当用户按下重做时,请从重做堆栈中弹出最后一个值。


我曾经在一款我写的象棋应用程序中使用了这种确切的方法,它非常有效!它使事情保持简单、有条理。不需要试图记住撤销堆栈的索引,而且清除重做堆栈也很容易。+1 - Sylvester Kruin

1

基于 Evgeny 的答案,使用自定义的 Entry,但添加了一个 tkinter 的 StringVar,并使用 trace 跟踪小部件,以更准确地跟踪其内容何时发生更改(而不仅仅是按下任何键,这似乎会向堆栈中添加空的撤消/重做项)。还使用 Python deque 添加了最大深度。

如果我们通过代码而不是键盘输入来更改 Entry 的内容,我们可以暂时禁用 trace(例如,请参见下面的 undo 方法)。

代码:


class CEntry(tk.Entry):
    def __init__(self, master, **kw):
        super().__init__(master=master, **kw)
        self._undo_stack = deque(maxlen=100)
        self._redo_stack = deque(maxlen=100)
        self.bind("<Control-z>", self.undo)
        self.bind("<Control-y>", self.redo)
        # traces whenever the Entry's contents are changed
        self.tkvar = tk.StringVar()
        self.config(textvariable=self.tkvar)
        self.trace_id = self.tkvar.trace("w", self.on_changes)
        self.reset_undo_stacks()
        # USE THESE TO TURN TRACE OFF THEN BACK ON AGAIN
        # self.tkvar.trace_vdelete("w", self.trace_id)
        # self.trace_id = self.tkvar.trace("w", self.on_changes)

    def undo(self, event=None):  # noqa
        if len(self._undo_stack) <= 1:
            return
        content = self._undo_stack.pop()
        self._redo_stack.append(content)
        content = self._undo_stack[-1]
        self.tkvar.trace_vdelete("w", self.trace_id)
        self.delete(0, tk.END)
        self.insert(0, content)
        self.trace_id = self.tkvar.trace("w", self.on_changes)

    def redo(self, event=None):  # noqa
        if not self._redo_stack:
            return
        content = self._redo_stack.pop()
        self._undo_stack.append(content)
        self.tkvar.trace_vdelete("w", self.trace_id)
        self.delete(0, tk.END)
        self.insert(0, content)
        self.trace_id = self.tkvar.trace("w", self.on_changes)

    def on_changes(self, a=None, b=None, c=None):  # noqa
        self._undo_stack.append(self.tkvar.get())
        self._redo_stack.clear()

    def reset_undo_stacks(self):
        self._undo_stack.clear()
        self._redo_stack.clear()
        self._undo_stack.append(self.tkvar.get())

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接