簡體   English   中英

交互式驗證 tkinter 中的條目小部件內容

[英]Interactively validating Entry widget content in tkinter

在 tkinter Entry小部件中交互式驗證內容的推薦技術是什么?

我已經閱讀了有關使用validate=Truevalidatecommand=command的帖子,並且這些功能似乎受到以下事實的限制:如果validatecommand命令更新了Entry小部件的值,它們就會被清除。

鑒於這種行為,我們是否應該綁定KeyPressCutPaste事件並通過這些事件監視/更新Entry小部件的值? (以及我可能錯過的其他相關事件?)

或者我們應該完全忘記交互式驗證,只對FocusOut事件進行驗證?

正確答案是,使用小部件的validatecommand屬性。 不幸的是,這個特性在 Tkinter 世界中被嚴重記錄不足,盡管它在 Tk 世界中被充分記錄。 即使它沒有很好地記錄,它也有您進行驗證所需的一切,而無需借助綁定或跟蹤變量,或在驗證過程中修改小部件。

訣竅是要知道您可以讓 Tkinter 將特殊值傳遞給您的 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方法時幕后發生的事情的更多信息,請參閱為什么 tkinter 輸入驗證需要調用 register()?

有關規范文檔,請參閱Tcl/Tk 條目手冊頁的驗證部分

在研究和試驗了 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,我很樂意接受任何和所有評論/建議。

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

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. 條目旨在通過實現“驗證命令”僅接受整數
  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 .

定義一個返回布爾值的函數,該布爾值指示輸入是否有效。
將其注冊為 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()

參考

在研究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()
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

如果您只想設置數字和最大字符,此代碼會有所幫助。

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

這是@Steven Rumbalski 通過跟蹤對StringVar的更改來驗證Entry小部件值的答案的改進版本——我已經通過就地編輯它進行了調試和一定程度的改進。

下面的版本將所有內容放入一個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()

回應orionrobert 處理通過選擇替換文本而不是單獨刪除或插入的簡單驗證的問題

所選文本的替換被處理為刪除后插入。 這可能會導致問題,例如,刪除應將光標移至左側,而替換應將光標移至右側。 幸運的是,這兩個過程是一個接一個執行的。 因此,我們可以區分刪除本身和由於替換而直接后跟插入的刪除,因為后者不會更改刪除和插入之間的空閑標志。

這是使用替代標志和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) ,我們可以回顧性地實現最想要的行為(因為我們的新替代標記與插入是一個新的、獨特的、最終的事件。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM