Validación interactiva del contenido del widget de entrada en tkinter

¿Cuál es la técnica recomendada para validar interactivamente el contenido en un widget de Entry tkinter?

He leído las publicaciones sobre el uso del validatecommand=command validate=True y validatecommand=command , y parece que estas funciones están limitadas por el hecho de que se borran si el comando validatecommand actualiza el valor del widget de Entry .

Dado este comportamiento, ¿debemos vincular los eventos KeyPress , Cut y Paste y monitorear / actualizar el valor de nuestro widget de Entry través de estos eventos? (¿Y otros eventos relacionados que podría haber perdido?)

¿O deberíamos olvidar por completo la validación interactiva y solo validar los eventos de FocusOut ?

La respuesta correcta es usar el atributo validatecommand del widget. Desafortunadamente, esta característica está muy poco documentada en el mundo Tkinter, aunque está suficientemente documentada en el mundo Tk. Aunque no está bien documentado, tiene todo lo que necesita para hacer la validación sin tener que recurrir a los enlaces o las variables de seguimiento, o modificar el widget desde dentro del procedimiento de validación.

El truco es saber que puede hacer que Tkinter pase valores especiales a su comando de validación. Estos valores le brindan toda la información que necesita saber para decidir si los datos son válidos o no: el valor anterior a la edición, el valor posterior a la edición si la edición es válida y varios otros bits de información. Para usar estos, sin embargo, necesitas hacer un poco de vudú para que esta información se pase a tu comando de validación.

Nota: es importante que el comando de validación devuelva True o False . Cualquier otra cosa hará que la validación se desactive para el widget.

Aquí hay un ejemplo que solo permite minúsculas (e imprime todos esos valores 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() 

Para obtener más información sobre lo que sucede debajo del capó cuando llama al método de register , consulte Validación de entrada tkinter

Utilice un Tkinter.StringVar para rastrear el valor del widget de entrada. Puede validar el valor de StringVar estableciendo un trace en él.

Aquí hay un breve progtwig de trabajo que solo acepta flotantes válidos en el widget de entrada.

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

Después de estudiar y experimentar con el código de Bryan, produje una versión mínima de validación de entrada. El siguiente código colocará un cuadro de entrada y solo aceptará dígitos numéricos.

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

Tal vez debería agregar que todavía estoy aprendiendo Python y con mucho gusto aceptaré todos y cada uno de los comentarios / sugerencias.

Mientras estudiaba la respuesta de Bryan Oakley , algo me dijo que se podría desarrollar una solución mucho más general. El siguiente ejemplo introduce una enumeración de modo, un diccionario de tipo y una función de configuración para fines de validación. Vea la línea 48 para ver un ejemplo de uso y una demostración de su simplicidad.

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

La respuesta de Bryan es correcta, sin embargo, nadie mencionó el atributo ‘invalidcommand’ del widget tkinter.

Una buena explicación está aquí: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Texto copiado / pegado en caso de enlace roto.

El widget de entrada también admite una opción de comando no válido que especifica una función de callback que se llama siempre que el comando validatecand devuelve False. Este comando puede modificar el texto en el widget usando el método .set () en la variable de texto asociada del widget. La configuración de esta opción funciona de la misma manera que la configuración del comando de validación. Debe usar el método .register () para envolver su función de Python; este método devuelve el nombre de la función envuelta como una cadena. Luego pasará como el valor de la opción invalidcommand esa cadena, o como el primer elemento de una tupla que contiene códigos de sustitución.

Nota: solo hay una cosa que no puedo entender cómo hacerlo: si agrega validación a una entrada, y el usuario selecciona una parte del texto y escribe un nuevo valor, no hay manera de capturar el valor original y restablecer la entrada. Aquí un ejemplo

  1. La entrada está diseñada para aceptar solo enteros implementando ‘validatecommand’
  2. El usuario entra 1234567
  3. El usuario selecciona ‘345’ y presiona ‘j’. Esto se registra como dos acciones: eliminación de ‘345’ e inserción de ‘j’. Tkinter ignora la eliminación y actúa solo en la inserción de ‘j’. ‘validatecommand’ devuelve False, y los valores pasados ​​a la función ‘invalidcommand’ son los siguientes:% d = 1,% i = 2,% P = 12j67,% s = 1267,% S = j
  4. Si el código no implementa una función ‘invalidcommand’, la función ‘validatecommand’ rechazará la ‘j’ y el resultado será 1267. Si el código implementa una función ‘invalidcommand’, no hay forma de recuperar el 1234567 original .

Respondiendo al problema de orionrobert de lidiar con la validación simple en las sustituciones de texto mediante la selección, en lugar de eliminaciones o inserciones separadas:

Una sustitución del texto seleccionado se procesa como una eliminación seguida de una inserción. Esto puede llevar a problemas, por ejemplo, cuando la eliminación debe mover el cursor hacia la izquierda, mientras que una sustitución debe mover el cursor hacia la derecha. Afortunadamente, estos dos procesos se ejecutan inmediatamente uno tras otro. Por lo tanto, podemos diferenciar entre una eliminación por sí misma y una eliminación seguida directamente por una inserción debido a una sustitución porque esta última no tiene tiempo de inactividad entre la eliminación y la inserción.

Esto se explota utilizando una substitutionFlag y un Widget.after_idle() . after_idle() ejecuta la función lambda al final de la cola de eventos:

 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: # restre 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 

Por supuesto, después de una sustitución, mientras se valida la parte de eliminación, uno todavía no sabrá si seguirá una inserción. Sin embargo, por suerte, con: .set() , .icursor() , .index(SEL_FIRST) , .index(SEL_LAST) , .index(INSERT) , podemos lograr el comportamiento más deseado retrospectivamente (ya que la combinación de nuestra nueva sustitución sustituye con una La inserción es un nuevo evento único y final.

Esta es una forma fácil de validar el valor de entrada, que permite al usuario ingresar solo dígitos:

 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 == "": 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 

PD: Este ejemplo puede ser muy útil para crear una aplicación como calc.