在Tkinter中交互式验证Entry小部件内容


85

交互验证tkinterEntry小部件中的内容的推荐技术是什么?

我已经阅读了有关使用validate=True和的文章validatecommand=command,并且看来这些功能受到以下事实的限制:如果validatecommand命令更新了Entry小部件的值,这些功能将被清除。

鉴于这种行为,我们应该绑定的KeyPressCut以及Paste事件和监视/更新我们的Entry小部件的价值,通过这件事情?(还有我可能错过的其他相关事件?)

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

Answers:


217

正确的答案是,使用validatecommand小部件的属性。不幸的是,尽管在Tkinter中已充分记录了此功能,但在Tkinter世界中却未充分记录此功能。即使没有很好地记录下来,它也具有验证所需的一切,而无需求助于绑定或跟踪变量,或在验证过程中修改窗口小部件。

诀窍是要知道您可以让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方法时幕后情况的更多信息,请参见输入验证tkinter


15
这是正确的方法。它解决了我尝试使jmeyer10的答案有效时发现的问题。与我在其他地方可以找到的相比,该示例提供了出色的文档进行验证。我希望我能给这5票。
Steven Rumbalski 2010年

3
哇!我同意史蒂文(Steven)的观点,这是一种值得一票之多的答复。您应该写一本关于Tkinter的书(并且您已经发布了足够的解决方案,以使该系列丛书更为丰富)。谢谢!!!
Malcolm

2
谢谢你的例子。值得注意的是validate命令必须返回一个布尔值(只有True和False)。如果没有,验证将被删除。
戴夫·巴彻

3
我认为此页面应该脱颖而出。
右腿

4
“在Tkinter世界中文档严重不足”。大声笑-与Tkiinter世界的几乎所有其他地方一样。
martineau

21

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


1
通常,人们使用entry.configure(validatecommand=...)并编写test_val而不是testVal,但这是一个很好的简单示例。
wizzwizz4

10

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

这是一个简短的工作程序,它仅接受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()

1
感谢您的帖子。我很高兴看到Tkinter StringVar .trace()方法正在使用中。
马尔科姆2010年

4

在研究布莱恩·奥克利(Bryan Oakley)的答案时,有人告诉我可以开发出更通用的解决方案。以下示例介绍了用于验证的模式枚举,类型字典和设置函数。有关用法及其简单说明,请参见第48行。

#! /usr/bin/env python3
# /programming/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()

4

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

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

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

Entry小部件还支持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 。

3

这是一种验证输入值的简单方法,该方法允许用户仅输入数字:

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

PS:此示例对于创建类似calc的应用程序非常有用。


2
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

2
嗨,欢迎来到Stack Overflow。“仅代码”答案不受欢迎,尤其是在回答已经有很多答案的问题时。请确保对您提供的回复为何具有实质性意义,而不是简单地呼应原始海报已经审核的内容,添加一些其他见解。
CHB

1
@Demian Wolf我喜欢您对原始答案的改进版本,但我不得不将其回滚。请考虑将其发布为您自己的答案(您可以在修订历史中找到它)。
Marc.2377

1

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

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

这是通过使用replaceFlag和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),我们可以回顾性达到最期望的行为(因为我们与插入新substitutionFlag的组合是一个新的唯一和最终的事件。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.