我想监视 tkinter
Text
小部件中的文本何时被修改,以便用户可以保存他们输入的任何新数据。然后按“保存”我想重置它。
我将
Text
小部件的 <<Modified>>
事件绑定到一个函数,以便对文本进行任何更改都会将“保存”按钮从 'disabled'
更新为 'normal'
状态。点击“保存”按钮后,我运行了一个函数,该函数重置 modified
标志并再次禁用“保存”按钮,直到进行进一步的更改。
但我发现它似乎只触发了一次事件。点击“保存”并没有将按钮重置为
'disabled'
状态,并且在第一次之后编辑文本似乎也没有影响“保存”按钮的状态。
下面是一个最小的示例,显示标志似乎没有被重置。
import tkinter as tk
root = tk.Tk()
def text_modified(event=None):
status_label.config(text="Modified = True")
def reset():
if not text_widget.edit_modified():
return
status_label.config(text="Modified = False")
text_widget.edit_modified(False)
text_widget = tk.Text(root, width=30, height=5)
text_widget.pack()
text_widget.bind("<<Modified>>", text_modified)
status_label = tk.Label(root, text="Modified = False")
status_label.pack()
reset_btn = tk.Button(root, text="Reset", command=reset)
reset_btn.pack()
root.mainloop()
事实证明,将
<<Modified>>
事件绑定到函数意味着该函数不会在 Text
小部件文本更改时运行,而是在 modified
标志更改时运行 - 无论它更改为 True
还是更改为False
。因此,我的“保存”按钮正在保存数据,禁用自身,并将 modified
标志重置为 False
,并且此标志更改触发了 <<Modified>>
事件,该事件绑定到再次启用“保存”按钮的函数。
这是一个最小的示例,显示了正在发生的事情。我们只需要调整我们绑定
<<Modified>>
事件的函数,以便它也能处理 modified
为 False
的情况:
import tkinter as tk
root = tk.Tk()
def modified_flag_changed(event=None):
if text_widget.edit_modified():
status_label.config(text="Modified = True")
print("Text modified")
else:
print("Modified flag changed to False")
def reset():
if not text_widget.edit_modified():
print("Doesn't need resetting")
return
status_label.config(text="Modified = False")
text_widget.edit_modified(False)
print('Reset')
text_widget = tk.Text(root, width=30, height=5)
text_widget.pack()
text_widget.bind("<<Modified>>", modified_flag_changed)
status_label = tk.Label(root, text="Modified = False")
status_label.pack()
reset_btn = tk.Button(root, text="Reset", command=reset)
reset_btn.pack()
root.mainloop()
使用 tkinter 解决像“...监视 tkinter
Text
小部件中的文本何时被修改...”这样的基本问题可能具有挑战性。
文本小部件的内置事件类型可以实现一些功能,但并不完全是我们想要的。首先,有
<Key>
和 <KeyPress>
事件类型,但这些事件类型会导致在按键修改文本框内容之前运行回调,这在大多数用例中是无用的。在回调中执行 .get("1.0", "end")
会产生旧内容,而不会因按键而造成修改。其次,有 <KeyRelease>
事件类型,但在这种类型中,对于大多数用例来说,回调运行得太晚,并且用户的键盘打字体验可能不够。第三,有 <<Modified>>
事件类型,但此事件类型仅导致回调有时运行,正如您在问题中所描述的那样。
一个解决方案可能是实现两个回调链,该链包括:第一个回调处理
<Key>
事件并执行指向第二个回调的 after_idle
命令;以及处理修改后的文本的第二个回调。
import tkinter as tk
from tkinter.scrolledtext import ScrolledText
class TextBox(ScrolledText):
def __init__(self, *args, **kwargs) -> None:
ScrolledText.__init__(self, *args, **kwargs)
# The next two lines are needed for the two callbacks.
self._execution_id = "" # this one is needed for the first callback
self.edit_modified(False) # and this one is need for the second callback
self.bind("<Key>", self.on_key) # Binding to <KeyPress> may work too.
# First callback:
def on_key(self, event: tk.Event) -> None:
# Firstly, we start by cancelling any scheduled executions of the second
# callback that have not yet been executed.
if self._execution_id:
self.after_cancel(self._execution_id)
# Secondly, we schedule an execution of the second callback using the
# `after_idle` command. (see https://tcl.tk/man/tcl8.5/TclCmd/after.htm#M9)
self._execution_id = self.after_idle(self.on_modified, event)
# Second callback:
def on_modified(self, event: tk.Event) -> None:
# Firstly, we use the built-in `edit_modified` method to check if the
# content of the text widget has changed. (see https://tcl.tk/man/tcl8.5/TkCmd/text.htm#M93)
# Otherwise, we abort the second callback. This is necessary to filter out
# key presses that do not change the contents of the Text widget, such as
# pressing arrow keys, Ctrl, Alt, etc.
if not self.edit_modified():
return
# Secondly, we do whatever we want with the content of the Text widget.
content = self.get("1.0", "end")
print("! on_modified:", repr(content))
# Thirdly, we set the built-in modified flag of the Text widget to False.
self.edit_modified(False)
if __name__ == "__main__":
root = tk.Tk()
text = TextBox(root, height=10, width=50)
text.pack(fill="both", expand=True, padx=10, pady=10)
text.focus_set()
root.mainloop()
这个两个回调链 (C2C) 方法适用于基本文本编辑按键,例如按字母键。重要的是(至少对于我的用例),当您在文本小部件中按 Ctrl+V 某些内容时(如果在文本小部件中设置了文本选择,则独立),此方法可以正常工作,不会引发丑陋的异常。我尝试过其他方法(自称为“Tk voodoo”),这些方法使用
rename
命令并通过代理生成虚拟事件,但这些方法无法处理 Ctrl+V,并且我的源很快变得难以理解的混乱(或在至少比平常快!)。
这种 C2C 方法需要很少的代码,而且它使用 tkinter 的公共方法,而不需要太深入 tkinter 的内部机制,这非常好。缺点可能是它需要
self._execution_id
属性,这可能会污染 TextBox 类的命名空间。也许,有更好的方法。