additive additive - 24 days ago 6
Python Question

Python tkinter text modified callback

In python 2.7, I am trying to get a callback every time something is changed in the Tkinter Text widget.

The program uses multiple frames based on code found here: Switch between two frames in tkinter

The callback part is taken from the following example: http://code.activestate.com/recipes/464635-call-a-callback-when-a-tkintertext-is-modified/

Both codes work fine separately, but combining those two is difficult for me.
Here is my attempt with as bare bones code as possible.

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()


I tried taking only the necessary parts from the callback example.
The code does do a callback (if self.tk.call(self._w, 'edit', 'modified', 0) line is commented out) when the text is modified, but resetting the modified flag does not work, so only the first modification is registered.

At the moment I get the following error:

line 67, in clearModifiedFlag
self.tk.call(self._w, 'edit', 'modified', 0)
_tkinter.TclError: bad option "edit": must be cget or configure


In the callback example code "edit" works fine.

Edit: This is the working code

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()

Answer

I suggest a simpler approach. You can set up a proxy for the widget, and within that proxy you can detect whenever anything was inserted or deleted. You can use that information to generate a virtual event, which can be bound to like any other event.

Let's start by creating a custom text widget class, which you will use like any other text widget:

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

The proxy in this example does three things:

  1. First it calls the actual widget command, passing in all of the arguments it received.
  2. Next it generates an event for every insert and every delete
  3. Then it then generates a virtual event
  4. And finally it returns the results of the actual widget command

You can use this widget exactly like any other Text widget, with the added benefit that you can bind to <<TextModified>>.

For example, if you wanted to display the number of characters in the text widget you could do something like this:

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()