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.
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.
StringVar() is a class from tkinter. It's used so that you can easily monitor changes to tkinter variables if they occur through the example code provided: def callback(*args): print "variable changed!" var = StringVar() var.trace("w", callback) var. set("hello")
In the case of textvariable , which is mostly used with Entry and Label widgets, it is a variable that will be displayed as text. When the variable changes, the text of the widget changes as well.
_ClassType IntVarConstruct an integer variable. set(self, value) Set the variable to value, converting booleans to integers. get(self) Return the value of the variable as an integer.
Tkinter events are executed at runtime and when we bind these events with a button or key, then we will get access to prioritize the event in the application. To bind the <Enter> key with an event in Tkinter window, we can use bind('<Return>', callback) by specifying the key and the callback function as the arguments.
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With