【问题标题】:Interactively validating Entry widget content in tkinter在 tkinter 中交互式验证条目小部件内容
【发布时间】:2011-05-07 15:03:57
【问题描述】:

在 tkinter Entry 小部件中交互式验证内容的推荐技术是什么?

我已阅读有关使用 validate=Truevalidatecommand=command 的帖子,这些功能似乎受到以下事实的限制:如果 validatecommand 命令更新 Entry 小部件的值,它们会被清除。

鉴于这种行为,我们是否应该绑定KeyPressCutPaste 事件并通过这些事件监控/更新我们的Entry 小部件的值? (以及我可能错过的其他相关活动?)

或者我们应该完全忘记交互式验证,只对FocusOut 事件进行验证?

【问题讨论】:

    标签: python validation tkinter textbox


    【解决方案1】:

    正确答案是,使用小部件的validatecommand 属性。不幸的是,这个特性在 Tkinter 世界中被严重记录不足,尽管它在 Tk 世界中被充分记录。即使没有很好地记录它,它也有您进行验证所需的一切,而无需借助绑定或跟踪变量,或在验证过程中修改小部件。

    诀窍是要知道您可以让 Tkinter 将特殊值传递给您的 validate 命令。这些值为您提供了决定数据是否有效所需的所有信息:编辑前的值、编辑后的值(如果编辑有效)以及其他一些信息。但是,要使用这些,您需要做一些巫术来将这些信息传递给您的 validate 命令。

    注意:验证命令返回TrueFalse 很重要。其他任何事情都会导致小部件的验证被关闭。

    这是一个只允许小写的例子。出于说明目的,它还打印所有特殊值的值。它们并非都是必需的。你很少需要超过一两个。

    import tkinter as tk  # python 3.x
    # import Tkinter as tk # python 2.x
    
    class Example(tk.Frame):
    
        def __init__(self, parent):
            tk.Frame.__init__(self, parent)
    
            # valid percent substitutions (from the Tk entry man page)
            # note: you only have to register the ones you need; this
            # example registers them all for illustrative purposes
            #
            # %d = Type of action (1=insert, 0=delete, -1 for others)
            # %i = index of char string to be inserted/deleted, or -1
            # %P = value of the entry if the edit is allowed
            # %s = value of entry prior to editing
            # %S = the text string being inserted or deleted, if any
            # %v = the type of validation that is currently set
            # %V = the type of validation that triggered the callback
            #      (key, focusin, focusout, forced)
            # %W = the tk name of the widget
    
            vcmd = (self.register(self.onValidate),
                    '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
            self.entry = tk.Entry(self, validate="key", validatecommand=vcmd)
            self.text = tk.Text(self, height=10, width=40)
            self.entry.pack(side="top", fill="x")
            self.text.pack(side="bottom", fill="both", expand=True)
    
        def onValidate(self, d, i, P, s, S, v, V, W):
            self.text.delete("1.0", "end")
            self.text.insert("end","OnValidate:\n")
            self.text.insert("end","d='%s'\n" % d)
            self.text.insert("end","i='%s'\n" % i)
            self.text.insert("end","P='%s'\n" % P)
            self.text.insert("end","s='%s'\n" % s)
            self.text.insert("end","S='%s'\n" % S)
            self.text.insert("end","v='%s'\n" % v)
            self.text.insert("end","V='%s'\n" % V)
            self.text.insert("end","W='%s'\n" % W)
    
            # Disallow anything but lowercase letters
            if S == S.lower():
                return True
            else:
                self.bell()
                return False
    
    if __name__ == "__main__":
        root = tk.Tk()
        Example(root).pack(fill="both", expand=True)
        root.mainloop()
    

    有关调用register 方法时幕后发生的更多信息,请参阅Why is calling register() required for tkinter input validation?

    有关规范文档,请参阅Validation section of the Tcl/Tk Entry man page

    【讨论】:

    • 这是正确的做法。它解决了我在尝试让 jmeyer10 的答案正常工作时发现的问题。与我在其他地方可以找到的相比,这个示例提供了更好的文档来验证。我希望我能给这 5 票。
    • 哇!我同意史蒂文的观点——这种答复值得一票以上。你应该写一本关于 Tkinter 的书(并且你已经发布了足够多的解决方案来使它成为一个多卷系列)。谢谢!!!
    • 我认为应该把this page 放在首位。
    • “在 Tkinter 世界中的记录严重不足”。大声笑——就像 Tkiinter 世界的几乎所有其他人一样。
    • @Rightleg 该页面不再存在。存档版本:web.archive.org/web/20190423043443/http://infohost.nmt.edu/tcc/…
    【解决方案2】:

    在研究和试验了 Bryan 的代码之后,我制作了一个最小版本的输入验证。以下代码将放置一个输入框,并且只接受数字。

    from tkinter import *
    
    root = Tk()
    
    def testVal(inStr,acttyp):
        if acttyp == '1': #insert
            if not inStr.isdigit():
                return False
        return True
    
    entry = Entry(root, validate="key")
    entry['validatecommand'] = (entry.register(testVal),'%P','%d')
    entry.pack()
    
    root.mainloop()
    

    也许我应该补充一点,我仍在学习 Python,我很乐意接受任何和所有 cmets/建议。

    【讨论】:

    • 一般人们使用entry.configure(validatecommand=...)并写成test_val而不是testVal,但这是一个很好的简单示例。
    【解决方案3】:

    使用Tkinter.StringVar 跟踪Entry 小部件的值。您可以通过在其上设置trace 来验证StringVar 的值。

    这是一个简短的工作程序,它只接受 Entry 小部件中的有效浮点数。

    try:
        from tkinter import *
    except ImportError:
        from Tkinter import *  # Python 2
    
    
    root = Tk()
    sv = StringVar()
    
    def validate_float(var):
        new_value = var.get()
        try:
            new_value == '' or float(new_value)
            validate_float.old_value = new_value
        except:
            var.set(validate_float.old_value)
    
    validate_float.old_value = ''  # Define function attribute.
    
    # trace wants a callback with nearly useless parameters, fixing with lambda.
    sv.trace('w', lambda nm, idx, mode, var=sv: validate_float(var))
    ent = Entry(root, textvariable=sv)
    ent.pack()
    ent.focus_set()
    
    root.mainloop()
    
    

    【讨论】:

    • 感谢您的帖子。我很高兴看到正在使用的 Tkinter StringVar .trace() 方法。
    • 知道为什么我可能会收到此错误吗? "NameError: name 'validate' is not defined"
    • @ArmenSanoyan:这是因为 validate 没有在这个 sn-p 中定义(应该更正)。
    • @Wolf:请参阅我所做的更新,因为答案的作者似乎对自己修复它不感兴趣......
    • @Wolf:在修复这里的问题时,我注意到了其他一些不足之处,因此决定发布我自己的answer 来解决这些问题。
    【解决方案4】:

    Bryan 的回答是正确的,但是没有人提到 tkinter 小部件的“invalidcommand”属性。

    这里有一个很好的解释: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

    在链接断开的情况下复制/粘贴文本

    Entry 小部件还支持一个 invalidcommand 选项,该选项指定一个回调函数,只要 validatecommand 返回 False,就会调用该回调函数。此命令可以通过使用 .set() 方法在小部件的关联文本变量上修改小部件中的文本。设置此选项的工作方式与设置 validatecommand 相同。你必须使用 .register() 方法来包装你的 Python 函数;此方法将包装函数的名称作为字符串返回。然后,您将传递该字符串或作为包含替换代码的元组的第一个元素作为 invalidcommand 选项的值。

    注意: 只有一件事我不知道该怎么做:如果您向条目添加验证,并且用户选择部分文本并键入新值,则无法捕获原始值并重置条目.这是一个例子

    1. Entry 旨在通过实现“validatecommand”仅接受整数
    2. 用户输入1234567
    3. 用户选择“345”并按“j”。这被注册为两个动作:删除“345”和插入“j”。 Tkinter 忽略删除,只对“j”的插入起作用。 'validatecommand' 返回 False,传递给 'invalidcommand' 函数的值如下: %d=1, %i=2, %P=12j67, %s=1267, %S=j
    4. 如果代码没有实现'invalidcommand'函数,'validatecommand'函数将拒绝'j',结果为1267。如果代码实现了'invalidcommand'函数,则无法恢复原来的 1234567。

    【讨论】:

      【解决方案5】:

      您可以创建一个函数,如果输入有效则返回True,否则返回False。然后您应该使用Tk.register 注册它,并将返回的Tcl 函数名称作为validatecommand 传递给Entry 小部件。

      例如(用户只能输入数字):

      import tkinter as tk
      
      root = tk.Tk()
      
      
      def on_validate(P):
          """Validates the input.
      
          Args:
              P (int): the value that the text will have if the change is allowed.
          
          Returns:
              bool: True if the input is digit-only or empty, and False otherwise.
          """
      
          return P.isdigit() or P == ""
      
      
      entry = tk.Entry(root)
      entry.grid(row=0, column=0)
      
      validate_callback = root.register(on_validate)
      entry.configure(validate="key",
                      validatecommand=(validate_callback, "%P"))
      
      root.mainloop()
      

      参考:https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/entry-validation.html

      【讨论】:

        【解决方案6】:

        在研究Bryan Oakley's answer 时,有件事告诉我可以开发出更通用的解决方案。以下示例介绍了用于验证目的的模式枚举、类型字典和设置函数。请参见第 48 行的用法示例及其简单性的演示。

        #! /usr/bin/env python3
        # https://stackoverflow.com/questions/4140437
        import enum
        import inspect
        import tkinter
        from tkinter.constants import *
        
        
        Mode = enum.Enum('Mode', 'none key focus focusin focusout all')
        CAST = dict(d=int, i=int, P=str, s=str, S=str,
                    v=Mode.__getitem__, V=Mode.__getitem__, W=str)
        
        
        def on_validate(widget, mode, validator):
            # http://www.tcl.tk/man/tcl/TkCmd/ttk_entry.htm#M39
            if mode not in Mode:
                raise ValueError('mode not recognized')
            parameters = inspect.signature(validator).parameters
            if not set(parameters).issubset(CAST):
                raise ValueError('validator arguments not recognized')
            casts = tuple(map(CAST.__getitem__, parameters))
            widget.configure(validate=mode.name, validatecommand=[widget.register(
                lambda *args: bool(validator(*(cast(arg) for cast, arg in zip(
                    casts, args)))))]+['%' + parameter for parameter in parameters])
        
        
        class Example(tkinter.Frame):
        
            @classmethod
            def main(cls):
                tkinter.NoDefaultRoot()
                root = tkinter.Tk()
                root.title('Validation Example')
                cls(root).grid(sticky=NSEW)
                root.grid_rowconfigure(0, weight=1)
                root.grid_columnconfigure(0, weight=1)
                root.mainloop()
        
            def __init__(self, master, **kw):
                super().__init__(master, **kw)
                self.entry = tkinter.Entry(self)
                self.text = tkinter.Text(self, height=15, width=50,
                                         wrap=WORD, state=DISABLED)
                self.entry.grid(row=0, column=0, sticky=NSEW)
                self.text.grid(row=1, column=0, sticky=NSEW)
                self.grid_rowconfigure(1, weight=1)
                self.grid_columnconfigure(0, weight=1)
                on_validate(self.entry, Mode.key, self.validator)
        
            def validator(self, d, i, P, s, S, v, V, W):
                self.text['state'] = NORMAL
                self.text.delete(1.0, END)
                self.text.insert(END, 'd = {!r}\ni = {!r}\nP = {!r}\ns = {!r}\n'
                                      'S = {!r}\nv = {!r}\nV = {!r}\nW = {!r}'
                                 .format(d, i, P, s, S, v, V, W))
                self.text['state'] = DISABLED
                return not S.isupper()
        
        
        if __name__ == '__main__':
            Example.main()
        

        【讨论】:

          【解决方案7】:
          import tkinter
          tk=tkinter.Tk()
          def only_numeric_input(e):
              #this is allowing all numeric input
              if e.isdigit():
                  return True
              #this will allow backspace to work
              elif e=="":
                  return True
              else:
                  return False
          #this will make the entry widget on root window
          e1=tkinter.Entry(tk)
          #arranging entry widget on screen
          e1.grid(row=0,column=0)
          c=tk.register(only_numeric_input)
          e1.configure(validate="key",validatecommand=(c,'%P'))
          tk.mainloop()
          #very usefull for making app like calci
          

          【讨论】:

          • 嗨,欢迎来到 Stack Overflow。 “仅代码”的答案不受欢迎,尤其是在回答已经有很多答案的问题时。请务必添加一些额外的见解,说明为什么您提供的回复在某种程度上是实质性的,而不是简单地呼应原始发帖人已经审查过的内容。
          • @Demian Wolf 我喜欢你对原始答案的改进版本,但我不得不回滚。请考虑将其发布为您自己的答案(您可以在revision history 中找到它)。
          【解决方案8】:

          回复orionrobert's problem 处理通过选择替换文本而不是单独删除或插入的简单验证:

          所选文本的替换被处理为删除后插入。这可能会导致问题,例如,删除应将光标移至左侧,而替换应将光标移至右侧。幸运的是,这两个过程立即一个接一个地执行。 因此,我们可以区分删除本身和由于替换而直接后跟插入的删除,因为后者不会更改删除和插入之间的空闲标志。

          这是使用替代标志和Widget.after_idle() 来利用的。 after_idle() 在事件队列末尾执行 lambda 函数:

          class ValidatedEntry(Entry):
              def __init__(self, *args, **kwargs):
                  super().__init__(*args, **kwargs)
          
                  self.tclValidate = (self.register(self.validate), '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
                  # attach the registered validation function to this spinbox
                  self.config(validate = "all", validatecommand = self.tclValidate)
          
              def validate(self, type, index, result, prior, indelText, currentValidationMode, reason, widgetName):
          
                  if typeOfAction == "0":
                      # set a flag that can be checked by the insertion validation for being part of the substitution
                      self.substitutionFlag = True
                      # store desired data
                      self.priorBeforeDeletion = prior
                      self.indexBeforeDeletion = index
                      # reset the flag after idle
                      self.after_idle(lambda: setattr(self, "substitutionFlag", False))
          
                      # normal deletion validation
                      pass
          
                  elif typeOfAction == "1":
          
                      # if this is a substitution, everything is shifted left by a deletion, so undo this by using the previous prior
                      if self.substitutionFlag:
                          # restore desired data to what it was during validation of the deletion
                          prior = self.priorBeforeDeletion
                          index = self.indexBeforeDeletion
          
                          # optional (often not required) additional behavior upon substitution
                          pass
          
                      else:
                          # normal insertion validation
                          pass
          
                  return True
          

          当然,在替换之后,在验证删除部分的同时,仍然不知道是否会出现插入。 然而幸运的是,有: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), 我们可以追溯地实现最期望的行为(因为我们的新替换标记与插入的组合是一个新的唯一和最终事件。

          【讨论】:

            【解决方案9】:

            如果您只想设置数字和最大字符,此代码会有所帮助。

            from tkinter import *
            
            root = Tk()
            
            def validate(P):
                if len(P) == 0 or len(P) <= 10 and P.isdigit():  # 10 characters
                    return True
                else:
                    return False
            
            ent = Entry(root, validate="key", validatecommand=(root.register(validate), '%P'))
            ent.pack()
            
            root.mainloop()
            

            【讨论】:

              【解决方案10】:

              这是@Steven Rumbalski 的answer 的改进版本,它通过跟踪对StringVar 的更改来验证Entry 小部件的值——我已经通过在适当的位置对其进行了调试和改进。

              下面的版本将所有内容放入StringVar子类中,以更好地封装正在发生的事情,更重要的是允许它的多个独立实例同时存在而不会相互干扰——a他的实现存在潜在问题,因为它使用函数属性而不是实例属性,这与全局变量本质上是相同的,并且在这种情况下可能会导致问题。

              try:
                  from tkinter import *
              except ImportError:
                  from Tkinter import *  # Python 2
              
              
              class ValidateFloatVar(StringVar):
                  """StringVar subclass that only allows valid float values to be put in it."""
              
                  def __init__(self, master=None, value=None, name=None):
                      StringVar.__init__(self, master, value, name)
                      self._old_value = self.get()
                      self.trace('w', self._validate)
              
                  def _validate(self, *_):
                      new_value = self.get()
                      try:
                          new_value == '' or float(new_value)
                          self._old_value = new_value
                      except ValueError:
                          StringVar.set(self, self._old_value)
              
              
              root = Tk()
              ent = Entry(root, textvariable=ValidateFloatVar(value=42.0))
              ent.pack()
              ent.focus_set()
              ent.icursor(END)
              
              root.mainloop()
              
              

              【讨论】:

              • 将其扩展到与多个实例一起工作是一项重要的改进,原来的“解决方案”在修复另一个问题时会引入大量问题(错误的代码结构)。
              • @Wolf:显然我同意。我觉得原作者使用 tkinter 的 StringVar 跟踪功能进行验证的想法是有好处的,但是发布的实现中的代码需要一些工作才能真正可行。
              猜你喜欢
              • 2019-06-22
              • 1970-01-01
              • 2022-12-08
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多