web-dev-qa-db-fra.com

Validation interactive du contenu du widget Entry dans tkinter

Quelle est la technique recommandée pour valider de manière interactive le contenu dans un widget tkinter Entry?

J'ai lu les articles sur l'utilisation de validate=True et validatecommand=command et il semble que ces fonctionnalités soient limitées par le fait qu'elles sont effacées si la commande validatecommand met à jour la valeur du widget Entry.

Compte tenu de ce comportement, devrions-nous lier les événements KeyPress, Cut et Paste et surveiller/mettre à jour la valeur de notre widget Entry via ces événements? (Et d'autres événements liés que j'ai peut-être manqués?)

Ou devrions-nous oublier complètement la validation interactive et ne valider que sur des événements FocusOut?

60
Malcolm

La bonne réponse est d'utiliser l'attribut validatecommand du widget. Malheureusement, cette fonctionnalité est gravement sous-documentée dans le monde Tkinter, bien qu’elle soit suffisamment documentée dans le monde Tk. Même s'il n'est pas bien documenté, il contient tout ce dont vous avez besoin pour effectuer la validation sans recourir à des liaisons, à des variables de traçage ou à la modification du widget depuis la procédure de validation.

L'astuce consiste à savoir que vous pouvez faire en sorte que Tkinter transmette des valeurs spéciales à votre commande de validation. Ces valeurs vous donnent toutes les informations dont vous avez besoin pour savoir si les données sont valides ou non: la valeur avant la modification, la valeur après la modification si la modification est valide et plusieurs autres bits d'information. Pour les utiliser, cependant, vous devez faire un peu de vaudou pour que ces informations soient transmises à votre commande de validation.

Remarque: il est important que la commande de validation retourne soit True, soit False. Tout le reste entraînera la validation de la validation pour le widget.

Voici un exemple qui n'autorise que les minuscules (et affiche toutes ces valeurs géniales):

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 __== "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()
155
Bryan Oakley

Utilisez un Tkinter.StringVar pour suivre la valeur du widget Entrée. Vous pouvez valider la valeur de la StringVar en définissant une trace dessus.

Voici un court programme de travail qui accepte uniquement les flottants valides dans le widget Entrée.

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

Après avoir étudié et expérimenté le code de Bryan, j'ai produit une version minimale de la validation des entrées. Le code suivant affichera une zone de saisie et acceptera uniquement les chiffres numériques.

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

Je devrais peut-être ajouter que j'apprends encore Python et que j'accepterai volontiers tous les commentaires/suggestions.

6
user1683793

La réponse de Bryan est correcte, mais personne n'a mentionné l'attribut 'invalidcommand' du widget tkinter.

Voici une bonne explication: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Texte copié/collé en cas de lien cassé

Le widget Entrée prend également en charge une option invalidcommand qui spécifie une fonction de rappel appelée chaque fois que la commande validatecommande renvoie False. Cette commande peut modifier le texte du widget en utilisant la méthode .set () sur la variable de texte associée au widget. La configuration de cette option fonctionne de la même manière que la configuration de la commande validatecommand. Vous devez utiliser la méthode .register () pour envelopper votre fonction Python; cette méthode renvoie le nom de la fonction encapsulée sous forme de chaîne. Ensuite, vous passerez comme valeur de l'option invalidcommand soit cette chaîne, soit le premier élément d'un tuple contenant des codes de substitution.

Remarque: Il n’ya qu’une chose que je ne puisse pas comprendre: Si vous ajoutez une validation à une entrée et que l’utilisateur sélectionne une partie du texte et saisit une nouvelle valeur, il n’existe aucun moyen de capturer l’original. valeur et réinitialiser l'entrée. Voici un exemple

  1. L'entrée est conçue pour n'accepter que les entiers en implémentant 'validatecommand'
  2. L'utilisateur entre 1234567
  3. L'utilisateur sélectionne '345' et appuie sur 'j'. Ceci est enregistré comme deux actions: suppression de '345' et insertion de 'j'. Tkinter ignore la suppression et n'agit que sur l'insertion de 'j'. 'validatecommand' renvoie False et les valeurs transmises à la fonction 'invalidcommand' sont les suivantes:% d = 1,% i = 2,% P = 12j67,% s = 1267,% S = j
  4. Si le code n'implémente pas de fonction 'invalidcommand', la fonction 'validatecommand' rejettera le 'j' et le résultat sera 1267. Si le code implémente une fonction 'invalidcommand', il n'existe aucun moyen de récupérer le 1234567 d'origine. .
2
orionrobert

En étudiant la réponse de Bryan Oakley , quelque chose m'a dit qu'une solution beaucoup plus générale pourrait être développée. L'exemple suivant présente une énumération de mode, un dictionnaire de types et une fonction d'installation à des fins de validation. Voir la ligne 48 pour un exemple d'utilisation et une démonstration de sa simplicité.

#! /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 __== '__main__':
    Example.main()
2
Noctis Skytower

Voici un moyen simple de valider la valeur d’entrée, qui permet à l’utilisateur de saisir uniquement des chiffres:

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: Cet exemple peut être très utile pour créer une application telle que calc.

0
Demian Wolf

Réponse à problème d’orionrobert de traiter avec une validation simple lors des substitutions de texte par sélection, au lieu de suppressions ou insertions séparées:

Une substitution de texte sélectionné est traitée comme une suppression suivie d'une insertion. Cela peut entraîner des problèmes, par exemple, lorsque la suppression doit déplacer le curseur vers la gauche, alors qu'une substitution doit déplacer le curseur vers la droite. Heureusement, ces deux processus sont exécutés immédiatement l'un après l'autre . Par conséquent, nous pouvons faire la différence entre une suppression par elle-même et une suppression suivie directement par une insertion due à une substitution car cette dernière n'a pas de temps mort et insertion. 

Ceci est exploité à l'aide d'un substitutionFlag et d'une option Widget.after_idle().after_idle(), exécutant la fonction lambda à la fin de la file d'attente des événements:

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

Bien sûr, après une substitution, lors de la validation de la suppression, on ne saura toujours pas si un insert suivra . Heureusement cependant, avec: .set(), .icursor() , .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), Nous pouvons obtenir le comportement le plus désiré rétrospectivement (car la combinaison de notre nouveau substitutionFlag avec une insertion est un nouvel événement unique et final.

0
Tdiddy
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
0
Mohammad Omar