Convalida interattiva del contenuto del widget Entry in tkinter


85

Qual è la tecnica consigliata per convalidare in modo interattivo il contenuto in un Entrywidget tkinter ?

Ho letto i post sull'utilizzo di validate=Truee validatecommand=commande sembra che queste funzionalità siano limitate dal fatto che vengono cancellate se il validatecommandcomando aggiorna il Entryvalore del widget.

Dato questo comportamento, dovremmo legare sui KeyPress, Cuted Pasteeventi e monitor / aggiornare il nostro Entryvalore del widget attraverso questi eventi? (E altri eventi correlati che potrei essermi perso?)

O dovremmo dimenticare del tutto la convalida interattiva e convalidare solo sugli FocusOuteventi?

Risposte:


221

La risposta corretta è, usa l' validatecommandattributo del widget. Sfortunatamente questa caratteristica è gravemente poco documentata nel mondo Tkinter, sebbene sia sufficientemente documentata nel mondo Tk. Anche se non è ben documentato, ha tutto il necessario per eseguire la convalida senza ricorrere a binding o variabili di tracciamento o modificare il widget dall'interno della procedura di convalida.

Il trucco è sapere che puoi fare in modo che Tkinter passi valori speciali al tuo comando di convalida. Questi valori forniscono tutte le informazioni necessarie per decidere se i dati sono validi o meno: il valore prima della modifica, il valore dopo la modifica se la modifica è valida e molti altri bit di informazioni. Per usarli, però, devi fare un po 'di voodoo per far passare queste informazioni al tuo comando di validazione.

Nota: è importante che il comando di convalida restituisca Trueo False. Qualsiasi altra cosa provocherà la disattivazione della convalida per il widget.

Ecco un esempio che consente solo il minuscolo (e stampa tutti quei valori funky):

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

Per ulteriori informazioni su ciò che accade dietro le quinte quando si chiama il registermetodo, vedere tkinter di convalida dell'input


16
Questo è il modo giusto per farlo. Risolve i problemi che ho riscontrato quando ho provato a far funzionare la risposta di jmeyer10. Questo esempio fornisce una documentazione superiore da convalidare rispetto a ciò che posso trovare altrove. Vorrei poter dare a questo 5 voti.
Steven Rumbalski

3
WOW! Sono d'accordo con Steven: questo è il tipo di risposta che merita più di un voto. Dovresti scrivere un libro su Tkinter (e hai già pubblicato abbastanza soluzioni per renderlo una serie in più volumi). Grazie!!!
Malcolm

2
Grazie per l'esempio. Vale la pena notare che validatecommand DEVE restituire un valore booleano (solo True e False). In caso contrario, la convalida verrà rimossa.
Dave Bacher

3
Penso che questa pagina dovrebbe essere portata in primo piano.
Gamba destra

4
"gravemente sotto-documentato nel mondo Tkinter". LOL - come quasi tutto il resto del mondo Tkiinter.
martineau

21

Dopo aver studiato e sperimentato con il codice di Bryan, ho prodotto una versione minima di convalida dell'input. Il codice seguente creerà una casella di immissione e accetterà solo cifre numeriche.

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

Forse dovrei aggiungere che sto ancora imparando Python e accetterò volentieri tutti i commenti / suggerimenti.


1
Generalmente le persone usano entry.configure(validatecommand=...)e scrivono test_valinvece di testVal, ma questo è un buon esempio semplice.
wizzwizz4

10

Utilizzare a Tkinter.StringVarper tenere traccia del valore del widget Entry. È possibile convalidare il valore di StringVarimpostando tracesu di esso.

Ecco un breve programma di lavoro che accetta solo float validi nel widget 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
Grazie per il tuo post Mi è piaciuto vedere il metodo Tkinter StringVar .trace () in uso.
Malcolm

qualche idea del perché potrei eventualmente ottenere questo errore? "NameError: il nome" validate "non è definito"
Armen Sanoyan

4

Mentre studiavo la risposta di Bryan Oakley , qualcosa mi diceva che poteva essere sviluppata una soluzione molto più generale. L'esempio seguente introduce un'enumerazione della modalità, un dizionario dei tipi e una funzione di configurazione a scopo di convalida. Vedere la riga 48 per un esempio di utilizzo e una dimostrazione della sua semplicità.

#! /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

La risposta di Bryan è corretta, tuttavia nessuno ha menzionato l'attributo "invalidcommand" del widget tkinter.

Una buona spiegazione è qui: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Copia / incolla del testo in caso di collegamento interrotto

Il widget Entry supporta anche un'opzione invalidcommand che specifica una funzione di callback che viene chiamata ogni volta che validatecommand restituisce False. Questo comando può modificare il testo nel widget utilizzando il metodo .set () sulla variabile di testo associata al widget. L'impostazione di questa opzione funziona come l'impostazione del comando validat. È necessario utilizzare il metodo .register () per eseguire il wrapping della funzione Python; questo metodo restituisce il nome della funzione racchiusa come stringa. Quindi passerai come valore dell'opzione invalidcommand quella stringa o come primo elemento di una tupla contenente codici di sostituzione.

Nota: c'è solo una cosa che non riesco a capire come fare: se aggiungi la convalida a una voce e l'utente seleziona una parte del testo e digita un nuovo valore, non c'è modo di catturare il valore originale e reimpostarlo l'entrata. Ecco un esempio

  1. La voce è progettata per accettare solo numeri interi implementando 'validatecommand'
  2. L'utente immette 1234567
  3. L'utente seleziona "345" e preme "j". Ciò viene registrato come due azioni: cancellazione di "345" e inserimento di "j". Tkinter ignora la cancellazione e agisce solo sull'inserimento di "j". 'validatecommand' restituisce False e i valori passati alla funzione 'invalidcommand' sono i seguenti:% d = 1,% i = 2,% P = 12j67,% s = 1267,% S = j
  4. Se il codice non implementa una funzione 'invalidcommand', la funzione 'validatecommand' rifiuterà la 'j' e il risultato sarà 1267. Se il codice implementa una funzione 'invalidcommand', non c'è modo di recuperare il 1234567 originale .

3

Ecco un modo semplice per convalidare il valore della voce, che consente all'utente di inserire solo cifre:

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: questo esempio può essere molto utile per creare un'app come 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
Ciao, benvenuto in Stack Overflow. Le risposte "solo codice" sono disapprovate, soprattutto quando si risponde a una domanda che ha già molte risposte. Assicurati di aggiungere ulteriori informazioni sul motivo per cui la risposta che stai fornendo è in qualche modo sostanziale e non semplicemente riecheggia ciò che è già stato controllato dal poster originale.
chb

1
@Demian Wolf Mi è piaciuta la tua versione migliorata della risposta originale, ma ho dovuto ripristinarla. Per favore, considera di pubblicarlo come una tua risposta (puoi trovarlo nella cronologia delle revisioni ).
Marc 2377

1

Rispondendo al problema di orionrobert di trattare con la semplice convalida su sostituzioni di testo tramite selezione, invece di cancellazioni o inserimenti separati:

Una sostituzione del testo selezionato viene elaborata come una cancellazione seguita da un inserimento. Ciò può portare a problemi, ad esempio, quando la cancellazione dovrebbe spostare il cursore a sinistra, mentre una sostituzione dovrebbe spostare il cursore a destra. Fortunatamente, questi due processi vengono eseguiti immediatamente dopo l'altro. Quindi, possiamo distinguere tra una cancellazione di per sé e una cancellazione seguita direttamente da un inserimento dovuto a una sostituzione perché quest'ultima non ha cambiato il flag di inattività tra cancellazione e inserimento.

Questo viene sfruttato utilizzando un substitutionFlag e un file Widget.after_idle(). after_idle()esegue la funzione lambda alla fine della coda degli eventi:

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

Naturalmente, dopo una sostituzione, mentre si convalida la parte di cancellazione, non si sa ancora se seguirà un inserimento. Per fortuna però, con: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), siamo in grado di raggiungere la maggior parte comportamento desiderato in modo retrospettivo (dal momento che la combinazione della nostra nuova substitutionFlag con un inserimento è un nuovo evento unico e finale.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.