Python 交互验证tkinter中的条目小部件内容

Python 交互验证tkinter中的条目小部件内容,python,validation,tkinter,textbox,Python,Validation,Tkinter,Textbox,在tkinterEntrywidget中交互验证内容的推荐技术是什么 我读过关于使用validate=True和validatecommand=command的帖子,这些功能似乎受到限制,因为如果validatecommand命令更新条目小部件的值,它们就会被清除 考虑到这种行为,我们是否应该绑定按键、剪切和粘贴事件,并通过这些事件监视/更新条目小部件的值?(以及我可能错过的其他相关事件?) 或者我们应该完全忘记交互式验证而只在FocusOut事件上进行验证吗?使用Tkinter.StringV

在tkinter
Entry
widget中交互验证内容的推荐技术是什么

我读过关于使用
validate=True
validatecommand=command
的帖子,这些功能似乎受到限制,因为如果
validatecommand
命令更新
条目
小部件的值,它们就会被清除

考虑到这种行为,我们是否应该绑定
按键
剪切
粘贴
事件,并通过这些事件监视/更新
条目
小部件的值?(以及我可能错过的其他相关事件?)


或者我们应该完全忘记交互式验证而只在
FocusOut
事件上进行验证吗?

使用
Tkinter.StringVar
跟踪条目小部件的值。您可以通过设置
跟踪来验证
StringVar
的值

下面是一个简短的工作程序,它只接受Entry小部件中的有效浮动

from Tkinter import *
root = Tk()
sv = StringVar()

def validate_float(var):
    new_value = var.get()
    try:
        new_value == '' or float(new_value)
        validate.old_value = new_value
    except:
        var.set(validate.old_value)    
validate.old_value = ''

# 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()

root.mainloop()

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

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

注意:验证命令必须返回
True
False
。任何其他操作都会导致关闭小部件的验证

下面是一个只允许小写的示例。为了便于说明,它还打印所有特殊值的值。它们并不都是必要的;你很少需要超过一两个

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
方法时发动机罩下发生的情况的更多信息,请参阅


有关规范文档,请参见

在研究和实验了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,我很乐意接受任何和所有的评论/建议。

在学习过程中,有一些事情告诉我,可以开发出一个更通用的解决方案。以下示例介绍了用于验证的模式枚举、类型字典和设置函数。参见第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()

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

这里有一个很好的解释:

链接断开时复制/粘贴文本

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

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

  • 条目通过实现“validatecommand”仅接受整数
  • 用户输入1234567
  • 用户选择“345”并按“j”。这被注册为两个操作:删除“345”和插入“j”。Tkinter忽略删除,仅在插入“j”时起作用validatecommand'返回False,传递给'invalidcommand'函数的值如下:%d=1,%i=2,%P=12j67,%s=1267,%s=j
  • 如果代码未实现“invalidcommand”功能,“validatecommand”功能将拒绝“j”,结果将为1267。如果代码确实实现了“invalidcommand”功能,则无法恢复原始1234567
  • 响应通过选择替换文本而不是单独删除或插入时处理简单验证的方法:

    选定文本的替换处理为删除,然后插入。这可能会导致问题,例如,当删除操作应将光标向左移动,而替换操作应将光标向右移动。幸运的是,这两个过程是紧接着执行的。 因此,我们可以区分删除本身和删除后直接插入(由于替换),因为后者没有改变删除和插入之间的空闲标志

    使用替换标志和
    小部件。after\u 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(选择优先)
    .index(SEL)_
    
    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
    
    import tkinter  # imports Tkinter module
    
    
    root = tkinter.Tk()  # creates a root window to place an entry with validation there
    
    
    def only_numeric_input(P):
        # checks if entry's value is an integer or empty and returns an appropriate boolean
        if P.isdigit() or P == "":  # if a digit was entered or nothing was entered
            return True
        return False
    
    
    my_entry = tkinter.Entry(root)  # creates an entry
    my_entry.grid(row=0, column=0)  # shows it in the root window using grid geometry manager
    callback = root.register(only_numeric_input)  # registers a Tcl to Python callback
    my_entry.configure(validate="key", validatecommand=(callback, "%P"))  # enables validation
    root.mainloop()  # enters to Tkinter main event loop
    
    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()