【问题标题】:tkinter.Text: binding a variable to widget text contentstkinter.Text:将变量绑定到小部件文本内容
【发布时间】:2014-02-02 04:51:36
【问题描述】:

对这个问题使用 Python 3.3 平台无关。

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

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

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

如何使用Text 小部件最好地模拟此变量绑定功能?我的想法是将tkinter.StringVar 绑定到Text 小部件,然后获取/设置所有文本。

更新:

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

以下是我的实验代码。为了与我最初的问题以及问题是如何解决的(?)完全相关,重要的几行是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() )

旁注:很明显,我来自基于间距的不同语言。对不起,我无能为力。

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

【问题讨论】:

    标签: python python-3.x tkinter


    【解决方案1】:

    如果您愿意冒险生活,则可以连接到文本小部件的内部,并让它在内容发生变化时调用一个函数,而不管它是如何变化的。

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

    有了这些,只需设置到该事件的绑定,并在变量上放置读取跟踪。当然,如果您尝试将小部件或图像插入到文本中,它们将不会反映在文本变量中。

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

    【讨论】:

    • Tcl。这是为 tkinter 提供动力的技术。 Tkinter 只是嵌入式 tcl 解释器上的一个瘦接口。
    • 知道为什么 TextWithVar 不允许将元素放在它上面吗?我尝试在它的右上角放置一个按钮,但是即使给定sticky='NE',按钮也会被吸引到底部而不是重叠。当我对普通的 Text 元素做同样的事情时,按钮重叠得很好。这几乎就像 TextWithVar 以某种方式忽略了网格。
    • @SamKrygsheld:我想不出什么会导致这种情况发生。我的猜测是你在两者之间做的事情略有不同。 TextWithVar Text,只是有额外的方法。什么都没有被带走。
    • @SamKrygsheld:cmets 不是进行扩展讨论的地方。如果您有特定问题,请单击“新问题”按钮。
    【解决方案2】:

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

    import tkinter as tk
    
    text_area = tk.Text(parent)
    
    text_area.bind('<KeyRelease>', lambda *args: do_something())
    

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

    【讨论】:

      【解决方案3】:

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

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

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

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

      给你:

      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)
      

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

      干杯!

      【讨论】:

      • &lt;&lt;Modified&gt;&gt; 事件并不奇怪,我认为您只是在期待它并非如此。它旨在让您的应用程序知道何时需要保存数据。在您重置之前,这是一次性事件。目的是使创建“需要保存”标志变得容易,因此您可以警告用户或在事情发生变化时自动保存。它从未被设计为每次更改时触发,只有当数据状态从已知状态(例如:当您从文件加载时)更改为更改状态时才触发。
      • 你是对的,当然,我完全误解了它的目的,并更新了响应以反映这一点。但是,由于某种原因,我的回答被否决了,除了滥用 > 标签之外,我真的看不出它有什么严重错误。至少它与未记录的功能无关。
      猜你喜欢
      • 2021-11-30
      • 2018-09-03
      • 2013-06-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-24
      相关资源
      最近更新 更多