tkinter.Text: binding a variable to widget text contents
python, python-3.x, tkinter
Solution
If you're willing to live dangerously, it's possible to hook in to the internals of the text widget, and have it call a function whenever the contents change, regardless of how it changed.
The trick is to replace the underlying tk widget command with a proxy. This proxy is responsible for doing whatever the real text widget would do, then send a virtual event if what it did was insert or delete text.
With that in place, it's just a matter of setting up a binding to that event, and putting a read trace on the variable. Of course, if you try inserting widgets or images into the text they won't be reflected in the textvariable.
Here's a quick and dirty example, not tested at all in anything real. This uses the same technique that I used to implement line numbers in a text widget (see https://stackoverflow.com/a/16375233)
import Tkinter as tk
import random
import timeit
class TextWithVar(tk.Text):
'''A text widget that accepts a 'textvariable' option'''
def __init__(self, parent, *args, **kwargs):
try:
self._textvariable = kwargs.pop("textvariable")
except KeyError:
self._textvariable = None
tk.Text.__init__(self, parent, *args, **kwargs)
# if the variable has data in it, use it to initialize
# the widget
if self._textvariable is not None:
self.insert("1.0", self._textvariable.get())
# this defines an internal proxy which generates a
# virtual event whenever text is inserted or deleted
self.tk.eval('''
proc widget_proxy {widget widget_command args} {
# call the real tk widget command with the real args
set result [uplevel [linsert $args 0 $widget_command]]
# if the contents changed, generate an event we can bind to
if {([lindex $args 0] in {insert replace delete})} {
event generate $widget <<Change>> -when tail
}
# return the result from the real widget command
return $result
}
''')
# this replaces the underlying widget with the proxy
self.tk.eval('''
rename {widget} _{widget}
interp alias {{}} ::{widget} {{}} widget_proxy {widget} _{widget}
'''.format(widget=str(self)))
# set up a binding to update the variable whenever
# the widget changes
self.bind("<<Change>>", self._on_widget_change)
# set up a trace to update the text widget when the
# variable changes
if self._textvariable is not None:
self._textvariable.trace("wu", self._on_var_change)
def _on_var_change(self, *args):
'''Change the text widget when the associated textvariable changes'''
# only change the widget if something actually
# changed, otherwise we'll get into an endless
# loop
text_current = self.get("1.0", "end-1c")
var_current = self._textvariable.get()
if text_current != var_current:
self.delete("1.0", "end")
self.insert("1.0", var_current)
def _on_widget_change(self, event=None):
'''Change the variable when the widget changes'''
if self._textvariable is not None:
self._textvariable.set(self.get("1.0", "end-1c"))
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.textvar = tk.StringVar()
self.textvar.set("Hello, world!")
# create an entry widget and a text widget that
# share a textvariable; typing in one should update
# the other
self.entry = tk.Entry(self, textvariable=self.textvar)
self.text = TextWithVar(self,textvariable=self.textvar,
borderwidth=1, relief="sunken",
background="bisque")
self.entry.pack(side="top", fill="x", expand=True)
self.text.pack(side="top",fill="both", expand=True)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Problem
Using Python 3.3 platform independent for this question. For the `Entry` widget, you can bind a variable to this widget's text contents like so (note the `textvariable` parameter in `Entry` constructor): ``` var = tkinter.StringVar() entryField = tkinter.Entry(master, textvariable=var) e.pack() var.set("a new value") # entryField text now updated with this value s = var.get() # whatever text now appears in entryField ``` For the `Text` widget however, there is no such variable binding feature. Class `Text` definition should likely begin at line 2927 in %python dir%/Lib/tkinter/__init__.py for Python 3.3 in Windows releases if interested. How can I best emulate this variable binding feature with the `Text` widget? My idea is to bind a `tkinter.StringVar` to a `Text` widget and just get/set all text. Update: I ended up inheriting `tkinter.Frame` as a `Text` wrapper which takes in a `textvariable` constructor parameter expected as a `tkinter.Variable` instance. The only reason in my example below why I didn't inherit from `Text` is just because I wanted a scrollbar too, but that's not important. The following is my experimental code. For exact relevance to my original question and how the problem was resolved (?), the important lines are `self.textvariable.get = self.GetText` and `self.textvariable.set = self.SetText`. Basically, I'm overriding the passed-in `tkinter.Variable` object's get and set methods to my own devices... ``` class TextExtension( tkinter.Frame ): """Extends Frame. Intended as a container for a Text field. Better related data handling and has Y scrollbar now.""" def __init__( self, master, textvariable = None, *args, **kwargs ): self.textvariable = textvariable if ( textvariable is not None ): if not ( isinstance( textvariable, tkinter.Variable ) ): raise TypeError( "tkinter.Variable type expected, {} given.".format( type( textvariable ) ) ) self.textvariable.get = self.GetText self.textvariable.set = self.SetText # build self.YScrollbar = None self.Text = None super().__init__( master ) self.YScrollbar = tkinter.Scrollbar( self, orient = tkinter.VERTICAL ) self.Text = tkinter.Text( self, yscrollcommand = self.YScrollbar.set, *args, **kwargs ) self.YScrollbar.config( command = self.Text.yview ) self.YScrollbar.pack( side = tkinter.RIGHT, fill = tkinter.Y ) self.Text.pack( side = tkinter.LEFT, fill = tkinter.BOTH, expand = 1 ) def Clear( self ): self.Text.delete( 1.0, tkinter.END ) def GetText( self ): text = self.Text.get( 1.0, tkinter.END ) if ( text is not None ): text = text.strip() if ( text == "" ): text = None return text def SetText( self, value ): self.Clear() if ( value is not None ): self.Text.insert( tkinter.END, value.strip() ) ``` Side note: It's probably pretty obvious I'm coming from a different language based on spacing. I'm sorry, I can't help it. I think I answered my own question. Whether or not this is the right thing to do to override the known methods of `tkinter.Variable` objects passed into my functions like I just did is a separate question I'll have to ask/research even though this is a private bit of code that will never be used outside my app. And I acknowledge that this does beg the question whether this is an effective solution at all.