Interaktywne sprawdzanie poprawności zawartości widżetu Entry w tkinter

85

Jaka jest zalecana technika interaktywnego sprawdzania poprawności treści w Entrywidżecie tkinter ?

Przeczytałem posty dotyczące używania validate=Truei validatecommand=commandi wydaje się, że te funkcje są ograniczone przez fakt, że są usuwane, jeśli validatecommandpolecenie aktualizuje wartość Entrywidżetu.

Biorąc pod uwagę to zachowanie, powinniśmy wiązać na KeyPress, Cutoraz Pastezdarzeń i monitora / aktualizować nasze Entrywartości widgetu poprzez te wydarzenia? (I inne powiązane wydarzenia, które mogłem przegapić?)

A może powinniśmy całkowicie zapomnieć o interaktywnej walidacji i sprawdzać ją tylko na FocusOutzdarzeniach?

Malcolm
źródło

Odpowiedzi:

217

Prawidłowa odpowiedź brzmi: użyj validatecommandatrybutu widżetu. Niestety ta funkcja jest poważnie niedostatecznie udokumentowana w świecie Tkinter, chociaż jest wystarczająco udokumentowana w świecie Tk. Mimo że nie jest dobrze udokumentowany, zawiera wszystko, co jest potrzebne do walidacji bez uciekania się do powiązań lub zmiennych śledzenia lub modyfikowania widżetu z poziomu procedury walidacji.

Sztuczka polega na tym, aby wiedzieć, że Tkinter może przekazać specjalne wartości do polecenia walidacji. Te wartości zapewniają wszystkie informacje, które musisz znać, aby zdecydować, czy dane są prawidłowe, czy nie: wartość przed edycją, wartość po edycji, jeśli edycja jest prawidłowa, i kilka innych informacji. Aby z nich skorzystać, musisz jednak zrobić małe voodoo, aby przekazać te informacje do polecenia walidacji.

Uwaga: ważne jest, aby polecenie walidacji zwróciło albo TruelubFalse . Cokolwiek innego spowoduje wyłączenie weryfikacji widgetu.

Oto przykład, który dopuszcza tylko małe litery (i wyświetla wszystkie te funky wartości):

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

Aby uzyskać więcej informacji o tym, co dzieje się pod maską, gdy wywołujesz registermetodę, zobacz tkinter sprawdzania poprawności danych wejściowych

Bryan Oakley
źródło
15
To jest właściwy sposób, aby to zrobić. Rozwiązuje problemy, które znalazłem, kiedy próbowałem uzyskać odpowiedź jmeyer10. Ten jeden przykład zapewnia lepszą dokumentację do zweryfikowania w porównaniu z tym, co mogę znaleźć gdzie indziej. Chciałbym móc oddać te 5 głosów.
Steven Rumbalski
3
ŁAŁ! Zgadzam się ze Stevenem - taka odpowiedź zasługuje na więcej niż jeden głos. Powinieneś napisać książkę o Tkinter (i opublikowałeś już wystarczająco dużo rozwiązań, aby uczynić tę serię wielotomową). Dziękuję Ci!!!
Malcolm
2
Dzięki za przykład. Warto zauważyć, że validatecommand MUSI zwrócić wartość logiczną (tylko True i False). Jeśli nie, weryfikacja zostanie usunięta.
Dave Bacher
3
Myślę, że tę stronę należy wysunąć na pierwszy plan.
Prawa noga
4
„bardzo słabo udokumentowane w świecie Tkinter”. LOL - jak prawie cała reszta świata Tkiinter.
martineau
21

Po przestudiowaniu i eksperymentowaniu z kodem Bryana stworzyłem minimalną wersję walidacji danych wejściowych. Poniższy kod utworzy pole wejściowe i akceptuje tylko cyfry.

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

Może powinienem dodać, że nadal uczę się Pythona i chętnie przyjmę wszelkie uwagi / sugestie.

user1683793
źródło
1
Zwykle ludzie używają entry.configure(validatecommand=...)i piszą test_valzamiast tego testVal, ale to jest dobry, prosty przykład.
wizzwizz4
10

Użyj a, Tkinter.StringVaraby śledzić wartość widżetu Wejście. Możesz sprawdzić wartość StringVar, ustawiając tracena nim.

Oto krótki działający program, który akceptuje tylko prawidłowe elementy zmiennoprzecinkowe w widgecie Wejście.

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()
Steven Rumbalski
źródło
1
Dzięki za twój post. Podobał mi się widok w użyciu metody Tkinter StringVar .trace ().
Malcolm
4

Podczas studiowania odpowiedzi Bryana Oakleya coś mi powiedziało, że można opracować znacznie bardziej ogólne rozwiązanie. Poniższy przykład przedstawia wyliczenie trybu, słownik typów i funkcję konfiguracji do celów walidacji. Zobacz wiersz 48, na przykład użycie i demonstracja jego prostoty.

#! /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()
Noctis Skytower
źródło
4

Odpowiedź Bryana jest poprawna, jednak nikt nie wspomniał o atrybucie „invalidcommand” widżetu tkinter.

Oto dobre wyjaśnienie: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Kopiowanie / wklejanie tekstu w przypadku zerwanego linku

Widżet Entry obsługuje również opcję invalidcommand, która określa funkcję wywołania zwrotnego, która jest wywoływana za każdym razem, gdy validatecommand zwraca wartość False. To polecenie może modyfikować tekst w widgecie za pomocą metody .set () na zmiennej tekstowej powiązanej z widżetem. Skonfigurowanie tej opcji działa tak samo, jak ustawienie validatecommand. Musisz użyć metody .register (), aby opakować swoją funkcję Pythona; ta metoda zwraca nazwę opakowanej funkcji jako ciąg. Następnie jako wartość opcji invalidcommand przekażesz ten łańcuch lub jako pierwszy element krotki zawierającej kody podstawienia.

Uwaga: Jest tylko jedna rzecz, której nie potrafię zrobić: jeśli dodasz walidację do wpisu, a użytkownik zaznaczy część tekstu i wpisze nową wartość, nie ma możliwości przechwycenia oryginalnej wartości i zresetowania wejście. Oto przykład

  1. Entry jest zaprojektowany tak, aby akceptować tylko liczby całkowite poprzez implementację „validatecommand”
  2. Użytkownik wprowadza 1234567
  3. Użytkownik wybiera „345” i naciska „j”. Jest to rejestrowane jako dwie czynności: usunięcie „345” i wstawienie „j”. Tkinter ignoruje usunięcie i działa tylko po wstawieniu „j”. „validatecommand” zwraca False, a wartości przekazane do funkcji „invalidcommand” są następujące:% d = 1,% i = 2,% P = 12j67,% s = 1267,% S = j
  4. Jeśli kod nie implementuje funkcji „invalidcommand”, funkcja „validatecommand” odrzuci „j”, a wynikiem będzie 1267. Jeśli kod implementuje funkcję „invalidcommand”, nie ma możliwości odzyskania oryginalnego 1234567 .
orionrobert
źródło
3

Oto prosty sposób sprawdzenia poprawności wartości wejściowej, który pozwala użytkownikowi wprowadzić tylko cyfry:

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: Ten przykład może być bardzo przydatny do tworzenia aplikacji takiej jak calc.

Demian Wolf
źródło
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
Mohammad Omar
źródło
2
Cześć, witaj w Stack Overflow. Odpowiedzi typu „tylko kod” są mile widziane, zwłaszcza gdy odpowiadamy na pytanie, na które już jest wiele odpowiedzi. Pamiętaj, aby dodać dodatkowe informacje na temat tego, dlaczego udzielona przez Ciebie odpowiedź jest w jakiś sposób merytoryczna i nie odzwierciedla po prostu tego, co zostało już sprawdzone przez oryginalny plakat.
chb
1
@Demian Wolf Podobała mi się twoja ulepszona wersja oryginalnej odpowiedzi, ale musiałem ją wycofać. Rozważ zamieszczenie go jako własnej odpowiedzi (możesz ją znaleźć w historii zmian ).
Marc 2377
1

Odpowiadając na problem orionroberta polegający na radzeniu sobie z prostą walidacją przy podstawianiu tekstu przez selekcję, zamiast oddzielnych delecji lub wstawień:

Podstawienie zaznaczonego tekstu jest przetwarzane jako usunięcie, po którym następuje wstawienie. Może to prowadzić do problemów, na przykład, gdy usunięcie powinno przesunąć kursor w lewo, podczas gdy podstawienie powinno przesunąć kursor w prawo. Na szczęście te dwa procesy są wykonywane bezpośrednio po sobie. W związku z tym możemy odróżnić samo usunięcie od usunięcia, po którym bezpośrednio następuje wstawienie z powodu podstawienia, ponieważ to ostatnie nie zmienia flagi bezczynności między usunięciem a wstawieniem.

Jest to wykorzystywane przy użyciu substitutionFlag i a Widget.after_idle(). after_idle()wykonuje funkcję lambda na końcu kolejki zdarzeń:

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

Oczywiście po podstawieniu, podczas sprawdzania poprawności części do usunięcia, nadal nie wiadomo, czy nastąpi wstawka. Na szczęście jednak, ze: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), możemy osiągnąć najbardziej pożądane zachowanie retrospektywnie (ponieważ połączenie naszego nowego substitutionFlag z wstawka to nowy unikalny i ostateczne wydarzenie.

Stendert
źródło