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

85

¿Cuál es la técnica recomendada para validar contenido de forma interactiva en un Entrywidget tkinter ?

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

Teniendo en cuenta este comportamiento, debemos unir en los KeyPress, Cuty Pasteeventos y supervisar / actualizar nuestro Entryvalor del widget de a través de estos eventos? (¿Y otros eventos relacionados que podría haberme perdido?)

¿O deberíamos olvidar la validación interactiva por completo y solo validar en FocusOuteventos?

Malcolm
fuente

Respuestas:

217

La respuesta correcta es utilizar el validatecommandatributo 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 realizar la validación sin recurrir a enlaces o 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 antes de la edición, el valor después de la edición si la edición es válida y varios otros bits de información. Sin embargo, para usarlos, debe hacer un poco de vudú para que esta información pase a su comando de validación.

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

Aquí hay un ejemplo que solo permite minúsculas (e imprime todos esos valores originales):

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 bajo el capó cuando llama al registermétodo, consulte Validación de entrada tkinter

Bryan Oakley
fuente
15
Esta es la forma correcta de hacerlo. Aborda los problemas que encontré cuando intenté que funcionara la respuesta de jmeyer10. Este ejemplo proporciona documentación superior para validar en comparación con lo que puedo encontrar en otros lugares. Ojalá pudiera darle 5 votos.
Steven Rumbalski
3
¡GUAUU! Estoy de acuerdo con Steven: este es el tipo de respuesta que merece más de un voto. Debería escribir un libro sobre Tkinter (y ya ha publicado suficientes soluciones para convertirla en una serie de varios volúmenes). ¡¡¡Gracias!!!
Malcolm
2
Gracias por el ejemplo. Vale la pena señalar que el comando validate DEBE devolver un valor booleano (solo Verdadero y Falso). De lo contrario, se eliminará la validación.
Dave Bacher
3
Creo que esta página debería destacarse.
Pierna derecha
4
"muy poco documentado en el mundo de Tkinter". LOL - como casi todo el resto del mundo Tkiinter.
martineau
21

Después de estudiar y experimentar con el código de Bryan, produje una versión mínima de la 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()

Quizás debería agregar que todavía estoy aprendiendo Python y con gusto aceptaré todos y cada uno de los comentarios / sugerencias.

user1683793
fuente
1
Generalmente la gente usa entry.configure(validatecommand=...)y escribe en test_vallugar de testVal, pero este es un buen ejemplo simple.
wizzwizz4
10

Utilice a Tkinter.StringVarpara realizar un seguimiento del valor del widget de entrada. Puede validar el valor de StringVarestableciendo a traceen él.

Aquí hay un programa de trabajo corto 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()
Steven Rumbalski
fuente
1
Gracias por tu publicación. Disfruté viendo el método Tkinter StringVar .trace () en uso.
Malcolm
4

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 presenta una enumeración de modo, un diccionario de tipos y una función de configuración con fines de validación. Consulte la línea 48 para ver un ejemplo de uso y una demostración de su simplicidad.

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

La respuesta de Bryan es correcta, sin embargo, nadie mencionó el atributo 'comando inválido' 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 devolución de llamada que se llama siempre que el comando de validación devuelve False. Este comando puede modificar el texto en el widget usando el método .set () en la variable de texto asociada al widget. La configuración de esta opción funciona igual que la configuración del comando validate. Debe usar el método .register () para ajustar su función Python; este método devuelve el nombre de la función envuelta como una cadena. Luego, pasará como valor de la opción de comando no válido 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 averiguar cómo hacer: si agrega validación a una entrada y el usuario selecciona una parte del texto y escribe un nuevo valor, no hay forma de capturar el valor original y restablecer la entrada. Aquí hay un ejemplo

  1. La entrada está diseñada para aceptar solo enteros implementando 'validatecommand'
  2. El usuario ingresa 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 sobre 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 de 'comando inválido', la función 'validatecommand' rechazará la 'j' y el resultado será 1267. Si el código implementa una función de 'comando inválido', no hay forma de recuperar el 1234567 original .
Orionrobert
fuente
3

A continuación, se muestra una forma sencilla 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 == "":  # 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

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

Lobo Demian
fuente
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
fuente
2
Hola, bienvenido a Stack Overflow. Las respuestas de "solo código" están mal vistas, especialmente cuando se responde a una pregunta que ya tiene muchas respuestas. Asegúrese de agregar información adicional sobre por qué la respuesta que está brindando es de alguna manera sustantiva y no simplemente se hace eco de lo que ya ha sido examinado por el póster original.
chb
1
@Demian Wolf Me gustó tu versión mejorada de la respuesta original, pero tuve que revertirla. Por favor, considere publicarlo como una respuesta propia (puede encontrarlo en el historial de revisión ).
Marc.2377
1

Respondiendo al problema de Orionrobert de lidiar con la validación simple en sustituciones de texto mediante 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 generar 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 cambia la bandera inactiva entre eliminación e inserción.

Esto se explota usando un 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:
                # 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

Por supuesto, después de una sustitución, mientras se valida la parte de eliminación, uno aún no sabrá si seguirá una inserción. Sin embargo, afortunadamente, con: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), podemos alcanzar la mayoría comportamiento deseado de forma retrospectiva (ya que la combinación de nuestro nuevo substitutionFlag con una inserción es un nuevo evento único y definitivo.

Stendert
fuente