Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

tkinter.Text: binding a variable to widget text contents

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.

like image 261
bob-the-destroyer Avatar asked Feb 02 '14 04:02

bob-the-destroyer


People also ask

What is Tk StringVar ()?

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

What does text variable do in tkinter?

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.

What is IntVar () in tkinter?

_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.

How do you bind an entry box in tkinter?

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.


1 Answers

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()
like image 73
Bryan Oakley Avatar answered Sep 19 '22 21:09

Bryan Oakley