Python tkinter.Text:将变量绑定到小部件文本内容

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21507178/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-18 23:01:11  来源:igfitidea点击:

tkinter.Text: binding a variable to widget text contents

pythonpython-3.xtkinter

提问by bob-the-destroyer

Using Python 3.3 platform independent for this question.

对于这个问题,使用独​​立于 Python 3.3 平台的 Python 3.3 平台。

For the Entrywidget, you can bind a variable to this widget's text contents like so (note the textvariableparameter in Entryconstructor):

对于Entry小部件,您可以像这样将变量绑定到该小部件的文本内容(注意构造函数中的textvariable参数Entry):

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 Textwidget however, there is no such variable binding feature. Class Textdefinition should likely begin at line 2927 in %python dir%/Lib/tkinter/__init__.py for Python 3.3 in Windows releases if interested.

Text然而,对于小部件,没有这样的变量绑定功能。Text如果有兴趣,类定义应该从 %python dir%/Lib/tkinter/__init__.py 中的第 2927 行开始,用于 Windows 版本中的 Python 3.3。

How can I best emulate this variable binding feature with the Textwidget? My idea is to bind a tkinter.StringVarto a Textwidget and just get/set all text.

我怎样才能最好地用Text小部件模拟这个变量绑定功能?我的想法是将 a 绑定tkinter.StringVar到一个Text小部件,然后获取/设置所有文本。

Update:

更新:

I ended up inheriting tkinter.Frameas a Textwrapper which takes in a textvariableconstructor parameter expected as a tkinter.Variableinstance. The only reason in my example below why I didn't inherit from Textis just because I wanted a scrollbar too, but that's not important.

我最终继承tkinter.Frame为一个Text包装器,它接受一个textvariable预期作为tkinter.Variable实例的构造函数参数。在下面的示例中,我没有继承自的唯一原因Text只是因为我也想要一个滚动条,但这并不重要。

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.GetTextand self.textvariable.set = self.SetText. Basically, I'm overriding the passed-in tkinter.Variableobject's get and set methods to my own devices...

以下是我的实验代码。为了与我的原始问题完全相关以及问题是如何解决的 (?),重要的几行是self.textvariable.get = self.GetTextself.textvariable.set = self.SetText。基本上,我将传入tkinter.Variable对象的 get 和 set 方法覆盖到我自己的设备上......

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

我想我回答了我自己的问题。这是否是tkinter.Variable像我刚刚做的那样覆盖传递给我的函数的对象的已知方法的正确做法是一个单独的问题,我将不得不问/研究即使这是一个永远不会的私有代码在我的应用程序之外使用。我承认这确实引出了一个问题,这是否是一个有效的解决方案。

回答by Bryan Oakley

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.

诀窍是用代理替换底层 tk 小部件命令。这个代理负责做真实文本小部件会做的任何事情,然后如果它所做的是插入或删除文本,则发送一个虚拟事件。

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.

有了它,只需设置与该事件的绑定,并在变量上放置读取跟踪即可。当然,如果您尝试在文本中插入小部件或图像,它们将不会反映在 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)

这是一个快速而肮脏的例子,根本没有经过任何真实的测试。这使用了我用来在文本小部件中实现行号的相同技术(参见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()

回答by Nicklas B?rjesson

I saw that the class proposed in the question didn't actually handle the textvariable like typical Tkinter widgets do, so I took it upon myself to do somewhat of a rewrite to make it a "proper" widget. :-)

我看到问题中提出的类实际上并没有像典型的 Tkinter 小部件那样处理文本变量,所以我自己进行了一些重写以使其成为“正确的”小部件。:-)

Usually, a textvariable instance isn't tinkered with by the class it is passed to, rather its get() function is called when the variable is changed(detected via trace) and the set()-function is called via some internal hook. That way, it can be used by other widgets. Also, monkey patching is perhaps not the safest of practices.

通常,textvariable 实例不会被传递给它的类修改,而是在变量更改(通过跟踪检测)时调用它的 get() 函数,并且通过某个内部钩子调用 set() 函数。这样,它就可以被其他小部件使用。此外,猴子修补可能不是最安全的做法。

In this case the Text widgets bind-method and the <<Modified>>-tag is used. It is not a typical "on_change" event that keeps on firing, but rather trigger when someone modifies the content of the widget, there to provide a mechanism to help signaling that the value has been modified. So after each time it fires, one needs to reset it using Text.edit_modified(False) as seen in the text_modified and var_modified functions for it to fire again. But it works, I didn't get <<Change>>to work for me.

在这种情况下,使用 Text 小部件绑定方法和<<Modified>>-tag。这不是一个持续触发的典型“on_change”事件,而是在有人修改小部件的内容时触发,在那里提供一种机制来帮助发出值已被修改的信号。因此,每次触发后,都需要使用 Text.edit_modified(False) 重置它,如 text_modified 和 var_modified 函数中所示,以便再次触发。但它有效,我没有<<Change>>为我工作。

And finally, the trace of the textvariable is temporarily disabled in text_modified to avoid unwanted looping. Also, the unhook()-method should be called for cleaning up if the widget is used in cases when parent is explicitly deleted, like in a modal window to avoid problems. If not, it could be disregarded.

最后,在 text_modified 中暂时禁用 textvariable 的跟踪以避免不必要的循环。此外,如果在显式删除父级的情况下使用小部件,则应调用 unhook() 方法进行清理,例如在模态窗口中以避免出现问题。如果没有,则可以无视。

Here you go:

干得好:

from tkinter import Frame, Variable, Scrollbar, Text

from tkinter.constants import VERTICAL, RIGHT, LEFT, BOTH, END, Y

class TextExtension(Frame):
    """Extends Frame.  Intended as a container for a Text field.  Better related data handling
    and has Y scrollbar."""


    def __init__(self, master, textvariable=None, *args, **kwargs):

        super(TextExtension, self).__init__(master)
        # Init GUI

        self._y_scrollbar = Scrollbar(self, orient=VERTICAL)

        self._text_widget = Text(self, yscrollcommand=self._y_scrollbar.set, *args, **kwargs)
        self._text_widget.pack(side=LEFT, fill=BOTH, expand=1)

        self._y_scrollbar.config(command=self._text_widget.yview)
        self._y_scrollbar.pack(side=RIGHT, fill=Y)

        if textvariable is not None:
            if not (isinstance(textvariable, Variable)):
                raise TypeError("tkinter.Variable type expected, " + str(type(textvariable)) + " given.".format(type(textvariable)))
            self._text_variable = textvariable
            self.var_modified()
            self._text_trace = self._text_widget.bind('<<Modified>>', self.text_modified)
            self._var_trace = textvariable.trace("w", self.var_modified)

    def text_modified(self, *args):
            if self._text_variable is not None:
                self._text_variable.trace_vdelete("w", self._var_trace)
                self._text_variable.set(self._text_widget.get(1.0, END))
                self._var_trace = self._text_variable.trace("w", self.var_modified)
                self._text_widget.edit_modified(False)

    def var_modified(self, *args):
        self.set_text(self._text_variable.get())
        self._text_widget.edit_modified(False)

    def unhook(self):
        if self._text_variable is not None:
            self._text_variable.trace_vdelete("w", self._var_trace)


    def clear(self):
        self._text_widget.delete(1.0, END)

    def set_text(self, _value):
        self.clear()
        if (_value is not None):
            self._text_widget.insert(END, _value)

You can see the code in use, and using unhook, here, in the on_post_merge_sql-function.

您可以在on_post_merge_sql-function 中看到正在使用的代码,并在此处使用 unhook 。

Cheers!

干杯!

回答by double_j

Not sure if this is what you were trying to do, but this worked for me:

不确定这是否是您想要做的,但这对我有用:

import tkinter as tk

text_area = tk.Text(parent)

text_area.bind('<KeyRelease>', lambda *args: do_something())

Every time a key is released in the text widget, it will run do_something

每次在文本小部件中释放一个键时,它都会运行 do_something