在 tkinter 中以交互方式验证条目小组件内容

Interactively validating Entry widget content in tkinter

提问人:Malcolm 提问时间:11/10/2010 最后编辑:Paolo ForgiaMalcolm 更新时间:5/24/2022 访问量:112765

问:

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

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

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

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

Python 验证 Tkinter 文本框

评论


答:

14赞 Steven Rumbalski 11/10/2010 #1

使用 a 跟踪小组件的值。您可以通过在 上设置 来验证 的值。Tkinter.StringVarEntryStringVartrace

这是一个简短的工作程序,它只接受小部件中的有效浮点数。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()

评论

4赞 Malcolm 11/10/2010
谢谢你的帖子。我很高兴看到 Tkinter StringVar .trace() 方法的使用。
0赞 Armen Sanoyan 12/16/2020
知道为什么我可能会得到这个错误吗?“NameError:未定义名称'validate'”
2赞 martineau 2/7/2021
@ArmenSanoyan:这是因为此代码段中未定义(应更正)。validate
2赞 martineau 8/14/2021
@Wolf:请参阅我所做的更新,因为答案的作者似乎对自己修复它不感兴趣......
2赞 martineau 8/14/2021
@Wolf:在修复这里的问题时,我注意到了一些其他的缺陷,并决定发布我自己的答案来解决它们。
283赞 Bryan Oakley 11/10/2010 #2

正确答案是,使用小部件的属性。不幸的是,这个功能在 Tkinter 世界中被严重低估了,尽管它在 Tk 世界中得到了充分的记录。尽管它没有很好的文档记录,但它具有执行验证所需的一切,而无需诉诸绑定或跟踪变量,或从验证过程中修改小部件。validatecommand

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

注意:验证命令返回 or 是很重要的。任何其他内容都将导致关闭小组件的验证。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() 才能进行 tkinter 输入验证?register

有关规范文档,请参见 Tcl/Tk Entry 手册页的 Validation 部分

评论

21赞 Steven Rumbalski 11/10/2010
这是正确的方法。它解决了我在尝试让 jmeyer10 的答案工作时发现的问题。与我在其他地方可以找到的文档相比,这个示例提供了更好的文档来验证。我希望我能投出这 5 票。
9赞 Malcolm 11/10/2010
哇!我同意史蒂文的观点——这是值得不止一票的回复。你应该写一本关于 Tkinter 的书(而且你已经发布了足够多的解决方案,可以把它变成一个多卷系列)。谢谢!!!
5赞 Right leg 9/8/2017
我认为这个页面应该被放在首位。
8赞 martineau 8/16/2018
“在 Tkinter 世界中记录严重不足”。大声笑——就像几乎所有的 Tkiinter 世界一样。
5赞 Maximouse 2/17/2020
@Rightleg 该页面不再存在。存档版本: web.archive.org/web/20190423043443/http://infohost.nmt.edu/tcc/...
27赞 user1683793 2/22/2016 #3

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

评论

5赞 wizzwizz4 8/12/2018
通常人们使用 和 write 来代替 ,但这是一个很好的简单例子。entry.configure(validatecommand=...)test_valtestVal
0赞 martineau 6/26/2022
难道你不需要也允许一个小数点吗? →".".isdigit()False
0赞 Xbox One 4/23/2023
@martineau 回答者打算此代码仅接受“数字”。这特意不包括“.”。“.” 不是数字。该代码并不意味着接受所有数字,而是意味着接受“数字”。
5赞 Noctis Skytower 5/12/2016 #4

在研究布莱恩·奥克利(Bryan Oakley)的答案时,有人告诉我,可以开发一个更通用的解决方案。下面的示例介绍一个模式枚举、一个类型字典和一个用于验证目的的设置函数。请参阅第 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()
8赞 orionrobert 11/21/2018 #5

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

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

如果链接断开,则复制/粘贴文本

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

注意: 只有一件事我无法弄清楚该怎么做:如果向条目添加验证,并且用户选择文本的一部分并键入新值,则无法捕获原始值并重置条目。下面是一个示例

  1. 条目旨在通过实现“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。
4赞 Mohammad Omar 2/1/2019 #6
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

评论

3赞 chb 2/1/2019
嗨,欢迎来到 Stack Overflow。“纯代码”的答案是不受欢迎的,尤其是在回答一个已经有很多答案的问题时。请务必添加一些额外的见解,说明为什么您提供的回复在某种程度上是实质性的,而不是简单地呼应原始发帖人已经审查过的内容。
1赞 Marc.2377 6/16/2019
@Demian Wolf 我喜欢你对原始答案的改进版本,但我不得不回滚它。请考虑将其作为您自己的答案发布(您可以在修订历史记录中找到它)。
1赞 Stendert 2/21/2019 #7

回应 orionrobert 的问题,即通过选择而不是单独删除或插入文本来处理文本替换的简单验证:

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

使用 substitutionFlag 和 . 在事件队列的末尾执行 lambda-function:Widget.after_idle()after_idle()

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

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

7赞 Demian Wolf 6/16/2019 #8

定义一个函数,该函数返回一个布尔值,该布尔值指示输入是否有效。
将其注册为 Tcl 回调,并将回调名称作为 .
validatecommand

例如:

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

参考资料

4赞 Hengen Asdwdrasd 3/23/2021 #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()
4赞 martineau 8/14/2021 #10

这是 @Steven Rumbalski 的答案的改进版本,即通过跟踪对 a 的更改来验证小部件值——我已经通过就地编辑它来调试并在一定程度上进行了改进。EntryStringVar

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

评论

0赞 Wolf 8/14/2021
扩展它以使用多个实例是一个重要的改进,最初的“解决方案”在修复另一个实例时会引入一个巨大的问题(错误的代码结构)。
0赞 martineau 8/14/2021
@Wolf:显然我同意。我觉得原作者使用 tkinter 的跟踪功能进行验证的想法是有道理的,但是发布的实现中的代码需要一些工作才能真正可行。StringVar