Validar el contenido de una caja de texto en Tcl/Tk (tkinter)

Validar el contenido de una caja de texto en Tcl/Tk (tkinter)



Ya vimos en un artículo anterior cómo trabajar con cajas de texto (vía la clase ttk.Entry) en una aplicación de escritorio de Tcl/Tk. Ahora bien, no es una operación poco común la de querer validar el texto que el usuario escribe en una caja determinada, por ejemplo, para permitir únicamente números, fechas, u otros formatos o aplicar otro tipo de restricciones. Veamos, entonces, cómo conseguirlo.

Comencemos con el siguiente código, que crea una ventana con una caja de texto (entry).

from tkinter import ttk
import tkinter as tk

root = tk.Tk()
root.config(width=300, height=200)
root.title("Mi aplicación")
entry = ttk.Entry()
entry.place(x=50, y=50, width=150)
root.mainloop()

Supongamos que queremos que el usuario solo pueda ingresar números. Para ello, primero creamos una función que será invocada cada vez que el usuario escribe, pega o elimina un texto en la caja:

def validate_entry(text):
    return text.isdecimal()

El argumento, text, es la cadena que el usuario está intentando escribir o pegar en la caja. Si está escribiendo, seguramente será únicamente un carácter, mientras que si está pegando algo desde el cortapapeles, puede ser un texto. A partir de ese texto llamamos al método isdecimal() de las cadenas, que retorna True si el contenido está formado únicamente por números, False si contiene, además, otros carácteres. Por último, usamos el valor de retorno de isdecimal() como resultado de nuestra propia función. Esto es fundamental: cuando el resultado de validate_entry() sea False, Tcl/Tk no le permitirá al usuario ingresar el carácter o texto en cuestión.

Ahora indiquémosle a nuestra caja de texto cuál es la función que debe llamar para verificar que el contenido sea válido. Cambiemos la línea 7 del primer código por:

entry = ttk.Entry(
    validate="key",
    validatecommand=(root.register(validate_entry), "%S")
)

Clarifiquemos esto que se ve un poco extraño, pero es muy sencillo. El primer argumento validate le indica a la caja qué tipo de validación estamos buscando: "key" corresponde al ingreso de texto. El segundo es un poco más complejo: le pasamos una tupla que contiene la función que se va a ocupar de validar los datos (validate_entry(), que previamente debemos registrarla vía root.register()) y luego una cadena que denota qué información queremos recibir como argumento en dicha función ("%S" representa el texto ingresado por el usuario).

El código completo es:

from tkinter import ttk
import tkinter as tk

def validate_entry(text):
    return text.isdecimal()

root = tk.Tk()
root.config(width=300, height=200)
root.title("Mi aplicación")
entry = ttk.Entry(
    validate="key",
    validatecommand=(root.register(validate_entry), "%S")
)
entry.place(x=50, y=50, width=150)
root.mainloop()

Ejecutamos el programa y ¡voilá! Veremos que ya no se permiten ingresar otra cosa que números.

Avancemos un poco. ¿Qué ocurre si, además, queremos que la cantidad máxima de caracteres ingresados sea diez? Necesitaríamos tener en nuestra función validate_entry() una cadena con el texto previo de la caja más el texto que se está por ingresar, para poder calcular su longitud y retornar False en caso de que exceda los diez caracteres. Por ende, digamos que queremos ese dato en nuestra función, agregando la cadena "%P" al argumento validatecommand:

entry = ttk.Entry(
    validate="key",
    validatecommand=(root.register(validate_entry), "%S", "%P")
)

Y cambiemos la definición de nuestra función, que ahora recibirá dos argumentos:

def validate_entry(text, new_text):
    return text.isdecimal()

Es decir: text es el texto que el usuario quiere ingresar (¡antes de que efectivamente sea insertado!) y new_text es el texto que ya contenía la caja (si es que había alguno) más text. Apliquémosle la restricción de los diez caraceteres:

def validate_entry(text, new_text):
    # Primero chequear que el contenido total no exceda los diez caracteres.
    if len(new_text) > 10:
        return False
    # Luego, si la validación anterior no falló, chequear que el texto solo
    # contenga números.
    return text.isdecimal()

¡Excelente! Veamos un último ejemplo. Digamos que nuestra caja de texto solo acepta fechas en formato dd/mm/aaaa (por ejemplo, 19/03/2020). ¿Qué restricciones podemos aplicar? En primer lugar, seguramente, que el texto no puede superar los diez caracteres, igual que antes. Lo siguiente sería chequear que dd, mm y aaaa sean números decimales. Por último, que entre el día y el mes y entre el mes y el año haya una barra que funcione como separador.

def validate_entry(new_text):
    """
    Chequear que `new_text` esté en formato dd/mm/aaaa.
    """
    # Máximo de diez caracteres.
    if len(new_text) > 10:
        return False
    checks = []
    for i, char in enumerate(new_text):
        # En los índices 2 y 5 deben estar los caracteres "/".
        if i in (2, 5):
            checks.append(char == "/")
        else:
            # En el resto de los casos, la única restricción es que sean
            # números entre el 0 y el 9.
            checks.append(char.isdecimal())
    # `all()` retorna verdadero si todos los chequeos son verdaderos.
    return all(checks)

Como solo estamos usando el argumento new_text, vamos a modificar validatecommand en la creación del control:

entry = ttk.Entry(
    validate="key",
    # Solo necesitamos "%P".
    validatecommand=(root.register(validate_entry), "%P")
)

¡Perfecto! Ahora nuestra caja solamente acepta fechas en el formato dispuesto.

Otras opciones posibles para validatecommand son "%d", "%i" y "%s" (nótese en minúscula). "%d" provee un argumento que determina la acción que se está validando ("1" cuando se agrega texto, "0" cuando se borra), cosa particularmente útil si se quiere aplicar una validación cuando se agrega texto pero no cuando se elimina, o viceversa. "%i" indica la posición en la que se agrega el texto. "%s" (en minúscula) es el contenido de la caja previo a la modificación. Podemos observar todos estos valores con el siguiente código:

from tkinter import ttk
import tkinter as tk

def validate_entry(action, index, new_text, previous_text, text):
    """
    Chequear que `new_text` esté en formato dd/mm/aaaa.
    """
    print("Acción:", action)
    print("Índice (posición) en el que se quiere insertar el texto:", index)
    print("Texto si la validación es verdadera:", new_text)
    print("Contenido anterior de la caja:", previous_text)
    print("Texto que se quiere insertar:", text)
    return True

root = tk.Tk()
root.config(width=300, height=200)
root.title("Mi aplicación")
entry = ttk.Entry(
    validate="key",
    validatecommand=(
        root.register(validate_entry),
        "%d", "%i", "%P", "%s", "%S"
    )
)
entry.place(x=50, y=50, width=150)
root.mainloop()

En resumen, para validar la inserción o remoción de caracteres por parte del usuario en una caja de texto, debemos:

  1. Crear una función que retorne True (permitir) o False (no permitir).
  2. Indicar el nombre de la función y los argumentos que queremos que reciba ("%d", "%i", etc.) en validatecommand al momento de crear la caja de texto, tal como mostramos, usando root.register().
  3. Agregar el argumento validate="key", también al momento de crear la caja de texto.


Deja una respuesta