简体   繁体   English

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

[英]Interactively validating Entry widget content in tkinter

What is the recommended technique for interactively validating content in a tkinter Entry widget?在 tkinter Entry小部件中交互式验证内容的推荐技术是什么?

I've read the posts about using validate=True and validatecommand=command , and it appears that these features are limited by the fact that they get cleared if the validatecommand command updates the Entry widget's value.我已经阅读了有关使用validate=Truevalidatecommand=command的帖子,并且这些功能似乎受到以下事实的限制:如果validatecommand命令更新了Entry小部件的值,它们就会被清除。

Given this behavior, should we bind on the KeyPress , Cut , and Paste events and monitor/update our Entry widget's value through these events?鉴于这种行为,我们是否应该绑定KeyPressCutPaste事件并通过这些事件监视/更新Entry小部件的值? (And other related events that I might have missed?) (以及我可能错过的其他相关事件?)

Or should we forget interactive validation altogether and only validate on FocusOut events?或者我们应该完全忘记交互式验证,只对FocusOut事件进行验证?

The correct answer is, use the validatecommand attribute of the widget.正确答案是,使用小部件的validatecommand属性。 Unfortunately this feature is severely under-documented in the Tkinter world, though it is quite sufficiently documented in the Tk world.不幸的是,这个特性在 Tkinter 世界中被严重记录不足,尽管它在 Tk 世界中被充分记录。 Even though it's not documented well, it has everything you need to do validation without resorting to bindings or tracing variables, or modifying the widget from within the validation procedure.即使它没有很好地记录,它也有您进行验证所需的一切,而无需借助绑定或跟踪变量,或在验证过程中修改小部件。

The trick is to know that you can have Tkinter pass in special values to your validate command.诀窍是要知道您可以让 Tkinter 将特殊值传递给您的 validate 命令。 These values give you all the information you need to know to decide on whether the data is valid or not: the value prior to the edit, the value after the edit if the edit is valid, and several other bits of information.这些值为您提供了决定数据是否有效所需的所有信息:编辑前的值、编辑后的值(如果编辑有效)以及其他一些信息。 To use these, though, you need to do a little voodoo to get this information passed to your validate command.但是,要使用这些,您需要做一些巫术来将此信息传递给您的验证命令。

Note: it's important that the validation command returns either True or False .注意:验证命令返回TrueFalse很重要。 Anything else will cause the validation to be turned off for the widget.其他任何事情都会导致小部件的验证被关闭。

Here's an example that only allows lowercase.这是一个只允许小写的例子。 It also prints the values of all of the special values for illustrative purposes.出于说明目的,它还打印所有特殊值的值。 They aren't all necessary;它们并非都是必需的。 you rarely need more than one or two.你很少需要超过一两个。

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

For more information about what happens under the hood when you call the register method, see Why is calling register() required for tkinter input validation?有关调用register方法时幕后发生的事情的更多信息,请参阅为什么 tkinter 输入验证需要调用 register()?

For the canonical documentation see the Validation section of the Tcl/Tk Entry man page有关规范文档,请参阅Tcl/Tk 条目手册页的验证部分

After studying and experimenting with Bryan's code, I produced a minimal version of input validation.在研究和试验了 Bryan 的代码之后,我制作了一个最小版本的输入验证。 The following code will put up an Entry box and only accept numeric digits.以下代码将放置一个输入框,并且只接受数字。

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

Perhaps I should add that I am still learning Python and I will gladly accept any and all comments/suggestions.也许我应该补充一点,我仍在学习 Python,我很乐意接受任何和所有评论/建议。

Use a Tkinter.StringVar to track the value of the Entry widget.使用Tkinter.StringVar来跟踪Entry小部件的值。 You can validate the value of the StringVar by setting a trace on it.您可以通过在其上设置trace来验证StringVar的值。

Here's a short working program that accepts only valid floats in the Entry widget.这是一个简短的工作程序,它只接受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()

Bryan's answer is correct, however no one mentioned the 'invalidcommand' attribute of the tkinter widget. Bryan 的回答是正确的,但是没有人提到 tkinter 小部件的“invalidcommand”属性。

A good explanation is here: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html一个很好的解释在这里:http: //infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Text copy/pasted in case of broken link在链接断开的情况下复制/粘贴文本

The Entry widget also supports an invalidcommand option that specifies a callback function that is called whenever the validatecommand returns False. Entry 小部件还支持一个 invalidcommand 选项,该选项指定一个回调函数,只要 validatecommand 返回 False,就会调用该回调函数。 This command may modify the text in the widget by using the .set() method on the widget's associated textvariable.此命令可以通过使用 .set() 方法在小部件的关联文本变量上修改小部件中的文本。 Setting up this option works the same as setting up the validatecommand.设置此选项的工作方式与设置 validatecommand 相同。 You must use the .register() method to wrap your Python function;你必须使用 .register() 方法来包装你的 Python 函数; this method returns the name of the wrapped function as a string.此方法将包装函数的名称作为字符串返回。 Then you will pass as the value of the invalidcommand option either that string, or as the first element of a tuple containing substitution codes.然后,您将传递该字符串或作为包含替换代码的元组的第一个元素作为 invalidcommand 选项的值。

Note: There is only one thing that I cannot figure out how to do: If you add validation to an entry, and the user selects a portion of the text and types a new value, there is no way to capture the original value and reset the entry.注意:只有一件事我不知道该怎么做:如果您向条目添加验证,并且用户选择部分文本并键入新值,则无法捕获原始值并重置入口。 Here's an example这是一个例子

  1. Entry is designed to only accept integers by implementing 'validatecommand'条目旨在通过实现“验证命令”仅接受整数
  2. User enters 1234567用户输入 1234567
  3. User selects '345' and presses 'j'.用户选择“345”并按“j”。 This is registered as two actions: deletion of '345', and insertion of 'j'.这被注册为两个动作:删除“345”和插入“j”。 Tkinter ignores the deletion and acts only on the insertion of 'j'. Tkinter 忽略删除,只对“j”的插入起作用。 'validatecommand' returns False, and the values passed to the 'invalidcommand' function are as follows: %d=1, %i=2, %P=12j67, %s=1267, %S=j 'validatecommand' 返回 False,传递给 'invalidcommand' 函数的值如下: %d=1, %i=2, %P=12j67, %s=1267, %S=j
  4. If the code does not implement an 'invalidcommand' function, the 'validatecommand' function will reject the 'j' and the result will be 1267. If the code does implement an 'invalidcommand' function, there is no way to recover the original 1234567.如果代码没有实现'invalidcommand'函数,'validatecommand'函数会拒绝'j',结果是1267。如果代码确实实现了'invalidcommand'函数,没有办法恢复原来的1234567 .

Define a function returning a boolean that indicates whether the input is valid.定义一个返回布尔值的函数,该布尔值指示输入是否有效。
Register it as a Tcl callback, and pass the callback name to the widget as a validatecommand .将其注册为 Tcl 回调,并将回调名称作为validatecommand传递给小部件。

For example:例如:

import tkinter as tk


def validator(P):
    """Validates the input.

    Args:
        P (int): the value the text would have after the change.

    Returns:
        bool: True if the input is digit-only or empty, and False otherwise.
    """

    return P.isdigit() or P == ""


root = tk.Tk()

entry = tk.Entry(root)
entry.configure(
    validate="key",
    validatecommand=(
        root.register(validator),
        "%P",
    ),
)
entry.grid()

root.mainloop()

Reference . 参考

While studying Bryan Oakley's answer , something told me that a far more general solution could be developed.在研究Bryan Oakley 的回答时,一些事情告诉我可以开发出更通用的解决方案。 The following example introduces a mode enumeration, a type dictionary, and a setup function for validation purposes.以下示例介绍了用于验证目的的模式枚举、类型字典和设置函数。 See line 48 for example usage and a demonstration of its simplicity.请参见第 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()
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

This code can help if you want to set both just digits and max characters.如果您只想设置数字和最大字符,此代码会有所帮助。

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

Here's an improved version of @Steven Rumbalski's answer of validating the Entry widgets value by tracing changes to a StringVar — which I have already debugged and improved to some degree by editing it in place.这是@Steven Rumbalski 通过跟踪对StringVar的更改来验证Entry小部件值的答案的改进版本——我已经通过就地编辑它进行了调试和一定程度的改进。

The version below puts everything into a StringVar subclass to encapsulates what's going on better and, more importantly allow multiple independent instances of it to exist at the same time without interfering with each other — a potential problem with his implementation because it utilizes function attributes instead of instance attributes, which are essentially the same thing as global variables and can lead to problems in such a scenario.下面的版本将所有内容放入一个StringVar子类中,以更好地封装正在发生的事情,更重要的是允许它的多个独立实例同时存在而不会相互干扰——他的实现存在潜在问题,因为它利用函数属性而不是实例属性,它们本质上与全局变量相同,在这种情况下可能会导致问题。

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

Responding to orionrobert's problem of dealing with simple validation upon substitutions of text through selection, instead of separate deletions or insertions:回应orionrobert 处理通过选择替换文本而不是单独删除或插入的简单验证的问题

A substitution of selected text is processed as a deletion followed by an insertion.所选文本的替换被处理为删除后插入。 This may lead to problems, for example, when the deletion should move the cursor to the left, while a substitution should move the cursor to the right.这可能会导致问题,例如,删除应将光标移至左侧,而替换应将光标移至右侧。 Fortunately, these two processes are executed immediately after one another.幸运的是,这两个过程是一个接一个执行的。 Hence, we can differentiate between a deletion by itself and a deletion directly followed by an insertion due to a substitution because the latter has does not change the idle flag between deletion and insertion.因此,我们可以区分删除本身和由于替换而直接后跟插入的删除,因为后者不会更改删除和插入之间的空闲标志。

This is exploited using a substitutionFlag and a Widget.after_idle() .这是使用替代标志和Widget.after_idle()来利用的。 after_idle() executes the lambda-function at the end of the event queue: 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

Of course, after a substitution, while validating the deletion part, one still won't know whether an insert will follow.当然,在替换之后,在验证删除部分的同时,仍然不知道是否会出现插入。 Luckily however, with: .set() , .icursor() , .index(SEL_FIRST) , .index(SEL_LAST) , .index(INSERT) , we can achieve most desired behavior retrospectively (since the combination of our new substitutionFlag with an insertion is a new unique and final event.然而幸运的是,通过.set().icursor().index(SEL_FIRST).index(SEL_LAST).index(INSERT) ,我们可以回顾性地实现最想要的行为(因为我们的新替代标记与插入是一个新的、独特的、最终的事件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM