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