【问题标题】:Python tkinter text modified callbackPython tkinter 文本修改回调
【发布时间】:2026-01-13 08:00:01
【问题描述】:

在 python 2.7 中,每当 Tkinter Text 小部件中的某些内容发生更改时,我都会尝试获取回调。

该程序根据此处找到的代码使用多个帧:Switch between two frames in tkinter

回调部分取自以下示例:http://code.activestate.com/recipes/464635-call-a-callback-when-a-tkintertext-is-modified/

两个代码单独工作都很好,但是将这两个代码结合起来对我来说很困难。 这是我尝试使用尽可能简单的代码。

import Tkinter as tk

class Texter(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        container = tk.Frame(self)
        container.pack()

        self.frames = {}

        for F in (ConnectPage, EditorPage):
            frame = F(container, self)
            self.frames[F] = frame
            frame.grid(row=0, column=0, sticky="nsew")

        page_name = EditorPage.__name__
        self.frames[page_name] = frame
        self.show_frame(ConnectPage)


    def show_frame(self, cont):
        frame = self.frames[cont]
        frame.tkraise()

    def get_page(self, page_name):
        return self.frames[page_name]


class ConnectPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        button1 = tk.Button(self, text="SecondPage",
                            command=lambda: controller.show_frame(EditorPage))
        button1.grid(row=2, column=3, padx=15)


class EditorPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        self.text = tk.Text(self, height=25, width=80)
        self.text.grid(column=0, row=0, sticky="nw")

        button2 = tk.Button(self, text="FirstPage",
                            command=lambda: controller.show_frame(ConnectPage))
        button2.grid(row=2, column=3, padx=15)

        self.clearModifiedFlag()
        self.bind_all('<<Modified>>', self._beenModified)

    def _beenModified(self, event=None):
        if self._resetting_modified_flag: return

        self.clearModifiedFlag()
        print("Hello!")
        #self.beenModified(event)

    def clearModifiedFlag(self):
        self._resetting_modified_flag = True

        try:
            self.tk.call(self._w, 'edit', 'modified', 0)

        finally:
            self._resetting_modified_flag = False


if __name__ == '__main__':
    gui = Texter()
    gui.mainloop()

我尝试只从回调示例中提取必要的部分。 当文本被修改时,代码确实会执行回调(如果 self.tk.call(self._w, 'edit', 'modified', 0) 行被注释掉),但会重置修改的flag 不起作用,所以只注册第一个修改。

目前我收到以下错误:
第 67 行,在 clearModifiedFlag self.tk.call(self._w, 'edit', 'modified', 0) _tkinter.TclError: bad option "edit": must be cget or configure

在回调示例代码中“编辑”工作正常。

编辑:这是工作代码

import Tkinter as tk

class Texter(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        container = tk.Frame(self)
        container.pack()

        self.frames = {}

        for F in (ConnectPage, EditorPage):
            frame = F(container, self)
            self.frames[F] = frame
            frame.grid(row=0, column=0, sticky="nsew")

        page_name = EditorPage.__name__
        self.frames[page_name] = frame
        self.show_frame(ConnectPage)


    def show_frame(self, cont):
        frame = self.frames[cont]
        frame.tkraise()

    def get_page(self, page_name):
        return self.frames[page_name]


class ConnectPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        button1 = tk.Button(self, text="SecondPage",
                            command=lambda: controller.show_frame(EditorPage))
        button1.grid(row=2, column=3, padx=15)


class EditorPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        self.text = CustomText(self, height=25, width=80)
        self.text.grid(column=0, row=0, sticky="nw")
        self.text.bind("<<TextModified>>", self.onModification)

        button2 = tk.Button(self, text="FirstPage",
                            command=lambda: controller.show_frame(ConnectPage))
        button2.grid(row=2, column=3, padx=15)

    def onModification(self, event):
        print("Yellow!")


class CustomText(tk.Text):
    def __init__(self, *args, **kwargs):
        """A text widget that report on internal widget commands"""
        tk.Text.__init__(self, *args, **kwargs)

        # create a proxy for the underlying widget
        self._orig = self._w + "_orig"
        self.tk.call("rename", self._w, self._orig)
        self.tk.createcommand(self._w, self._proxy)

    def _proxy(self, command, *args):
        cmd = (self._orig, command) + args
        result = self.tk.call(cmd)

        if command in ("insert", "delete", "replace"):
            self.event_generate("<<TextModified>>")

        return result

if __name__ == '__main__':
    gui = Texter()
    gui.mainloop()

【问题讨论】:

    标签: python tkinter callback tkinter.text


    【解决方案1】:

    我建议一种更简单的方法。您可以为小部件设置代理,并且在该代理中,您可以检测插入或删除任何内容的时间。您可以使用该信息生成一个虚拟事件,该事件可以像任何其他事件一样绑定。

    让我们从创建一个自定义文本小部件类开始,您将像使用任何其他文本小部件一样使用它:

    import Tkinter as tk
    
    class CustomText(tk.Text):
        def __init__(self, *args, **kwargs):
            """A text widget that report on internal widget commands"""
            tk.Text.__init__(self, *args, **kwargs)
    
            # create a proxy for the underlying widget
            self._orig = self._w + "_orig"
            self.tk.call("rename", self._w, self._orig)
            self.tk.createcommand(self._w, self._proxy)
    
        def _proxy(self, command, *args):
            cmd = (self._orig, command) + args
            result = self.tk.call(cmd)
    
            if command in ("insert", "delete", "replace"):
                self.event_generate("<<TextModified>>")
    
            return result
    

    本例中的代理做了三件事:

    1. 首先它调用实际的小部件命令,传入它收到的所有参数。
    2. 接下来,它会为每次插入和每次删除生成一个事件
    3. 然后它会生成一个虚拟事件
    4. 最后它返回实际小部件命令的结果

    您可以像使用任何其他 Text 小部件一样使用此小部件,还可以绑定到 &lt;&lt;TextModified&gt;&gt;

    例如,如果您想在文本小部件中显示字符数,您可以执行以下操作:

    root = tk.Tk()
    label = tk.Label(root, anchor="w")
    text = CustomText(root, width=40, height=4)
    
    label.pack(side="bottom", fill="x")
    text.pack(side="top", fill="both", expand=True)
    
    def onModification(event):
        chars = len(event.widget.get("1.0", "end-1c"))
        label.configure(text="%s chars" % chars)
    
    text.bind("<<TextModified>>", onModification)
    
    root.mainloop()
    

    【讨论】:

    • 谢谢你的回答 Bryan,就像一个魅力! :) 你能否告诉我这种技术是如何被调用的(如果它有名字的话)。例如像多帧方法使用 MVC 架构。另一个问题,请您解释一下self._w是什么,它似乎很重要但找不到有关它的信息。
    • @additive: self._w 只是小部件的内部名称。该代码可以改用str(self)。我不确定整个技术是否有一个名称,尽管它使用代理模式作为实现的一部分。
    • 我想知道为什么 事件做同样的工作,但运行迟了一个阶段说如果我用 在文本中输入“Hello There”,我得到“Hello Ther”
    • @RahulARanger:有关如何处理事件的说明,请参阅此答案:*.com/questions/11541262/…
    【解决方案2】:

    我在我的代码中集成了上述&lt;&lt;TextModified>> 示例 工作得很好,除了它干扰了一些 edit_modified() 命令。

    幸运的是,tkinter Text 窗口的文档记录很差 一样好并且与edit_modified() get 完全兼容 或设置命令:预定义的&lt;&lt;Modified>> 标签。你甚至没有 要创建它,它是开箱即用的。

    以下是我的代码的相关部分:

    删除了“self”前缀,可能需要进行一些调整

    将其放入您的文本小工具代码中:

    title = set_title(fname, numbr)

    text.bind("&lt;&lt;Modified&gt;&gt;", lambda dummy: save_indicator(title))

    确保这些功能可见:

    def set_title(fname, numbr):  
        "Creates a window title showing the save indicator,"  
        "the file name and a window number"  
        fname = strip_path(fname)  
        if not fname:  
            fname = "(New Document)"  
        return "+ {} - Window no.{}".format(fname, numbr)  
    
    def strip_path(fname):  
        return os.path.split(fname)[-1]
    
    def save_indicator(title, event=None):  
        "Update the window title"  
        titre = toggle_star(title)  
        text.winfo_toplevel().title(title)  
    
    def toggle_star(title):  
        "Change the first character of the title"  
        chr='+'; chr0='x'  
        if text.edit_modified():  
            title = chr0 + title[1:]  
        else:  
            title = chr + title[1:]  
        return title  
    

    这是一个完整的工作示例,带有预定义的&lt;&lt;Modified>> 标签:

    def toggle_star(title):  
        "Change the color of the star in the title bar"  
        chr='+'; chr0='x'  
        if text.edit_modified():  
                title = chr0 + title[1:]  
        else:  
                title = chr + title[1:]  
        return title  
    
    def set_title(fname, winno):  
        "Put save indicator, file name and window number in the title"  
        if not fname:  
                fname = "(New Document)"  
        return "+ {} - Window no.{}".format(fname, winno)  
    
    def mockSave(title, event=None):  
        title = toggle_star(title)  
        root.winfo_toplevel().title(title)  
        text.edit_modified(0)  
    
    def read_ed_mod():  
        print("text.edit_modified()=", text.edit_modified())  
    
    def onModification(title, event=None):  
        title = toggle_star(title)  
        root.winfo_toplevel().title(title)  
    
    from tkinter import *  
    
    fname = 'blabla.txt'  
    winno = 1 ;  
    
    root = Tk()  
    label = Label(root, anchor="w")  
    text = Text(root, width=40, height=4)  
    label.pack(side="bottom", fill="x")  
    text.pack(side="top", fill="both", expand=True)  
    Button(root, text='Mock Save', command= lambda: mockSave(title)).pack(side=LEFT)  
    Button(root, text='Read ed_mod', command= lambda: read_ed_mod()).pack(side=RIGHT)  
    text.bind('<<Modified>>', lambda event: onModification(title))  
    
    title = set_title(fname, winno)  
    root.winfo_toplevel().title(title)  
    text.edit_modified(0)  
    
    root.mainloop()  
    

    【讨论】:

    • 感谢您的编辑,Mostafa!