Validando interativamente o conteúdo do widget Entry no tkinter

85

Qual é a técnica recomendada para validar interativamente o conteúdo em um Entrywidget tkinter ?

Eu li as postagens sobre o uso de validate=Truee validatecommand=command, e parece que esses recursos são limitados pelo fato de serem limpos se o validatecommandcomando atualizar o Entryvalor do widget.

Dado este comportamento, devemos ligar nos KeyPress, Cute Pasteeventos e monitor / atualizar o nosso Entryvalor do widget através destes eventos? (E outros eventos relacionados que eu possa ter perdido?)

Ou devemos esquecer a validação interativa completamente e apenas validar em FocusOuteventos?

Malcolm
fonte

Respostas:

221

A resposta correta é: use o validatecommandatributo do widget. Infelizmente, esse recurso está seriamente sub documentado no mundo Tkinter, embora seja suficientemente documentado no mundo Tk. Mesmo que não seja bem documentado, ele tem tudo que você precisa para fazer a validação sem recorrer a ligações ou rastrear variáveis, ou modificar o widget de dentro do procedimento de validação.

O truque é saber que você pode fazer com que o Tkinter passe valores especiais para o seu comando de validação. Esses valores fornecem todas as informações que você precisa saber para decidir se os dados são válidos ou não: o valor antes da edição, o valor após a edição se a edição for válida e vários outros bits de informação. Para usá-los, no entanto, você precisa fazer um pequeno vodu para que essas informações sejam passadas para seu comando de validação.

Nota: é importante que o comando de validação retorne Trueou False. Qualquer outra coisa fará com que a validação seja desativada para o widget.

Aqui está um exemplo que permite apenas letras minúsculas (e imprime todos os 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 obter mais informações sobre o que acontece nos bastidores quando você chama o registermétodo, consulte Validação de entrada tkinter

Bryan Oakley
fonte
16
Esta é a maneira certa de fazer isso. Ele aborda os problemas que encontrei quando tentei fazer a resposta de jmeyer10 funcionar. Este exemplo fornece documentação superior para validação em comparação com o que posso encontrar em outro lugar. Eu gostaria de poder dar 5 votos a isso.
Steven Rumbalski
3
UAU! Concordo com o Steven - este é o tipo de resposta que merece mais do que um voto. Você deve escrever um livro sobre o Tkinter (e você já postou soluções suficientes para torná-lo uma série de vários volumes). Obrigado!!!
Malcolm de
2
Obrigado pelo exemplo. É importante notar que o comando validat DEVE retornar um booleano (apenas True e False). Caso contrário, a validação será removida.
Dave Bacher
3
Acho que esta página deve ser trazida à tona.
perna direita
4
"gravemente sub documentado no mundo Tkinter". LOL - como quase todo o resto do mundo Tkiinter.
martineau de
21

Depois de estudar e experimentar o código de Bryan, produzi uma versão mínima de validação de entrada. O código a seguir abrirá uma caixa de entrada e aceitará apenas 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()

Talvez eu deva acrescentar que ainda estou aprendendo Python e terei prazer em aceitar todo e qualquer comentário / sugestão.

user1683793
fonte
1
Geralmente as pessoas usam entry.configure(validatecommand=...)e escrevem em test_valvez de testVal, mas este é um exemplo bom e simples.
wizzwizz4
10

Use a Tkinter.StringVarpara rastrear o valor do widget de entrada. Você pode validar o valor de StringVardefinindo um tracenele.

Aqui está um pequeno programa de trabalho que aceita apenas flutuações válidas no widget Entry.

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
fonte
1
Obrigado pela sua postagem. Gostei de ver o método Tkinter StringVar .trace () em uso.
Malcolm de
alguma ideia de por que eu poderia possivelmente receber esse erro? "NameError: name 'validate' is not defined"
Armen Sanoyan
4

Enquanto estudava a resposta de Bryan Oakley , algo me disse que uma solução muito mais geral poderia ser desenvolvida. O exemplo a seguir apresenta uma enumeração de modo, um dicionário de tipo e uma função de configuração para fins de validação. Veja a linha 48 para um exemplo de uso e uma demonstração de sua simplicidade.

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

A resposta de Bryan está correta, no entanto, ninguém mencionou o atributo 'invalidcommand' do widget tkinter.

Uma boa explicação está aqui: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Texto copiado / colado em caso de link quebrado

O widget Entry também oferece suporte a uma opção de comando inválido que especifica uma função de retorno de chamada que é chamada sempre que o comando validat retorna False. Este comando pode modificar o texto no widget usando o método .set () na variável de texto associada ao widget. A configuração desta opção funciona da mesma forma que a configuração do comando validat. Você deve usar o método .register () para envolver sua função Python; este método retorna o nome da função agrupada como uma string. Em seguida, você passará como o valor da opção de comando inválido essa string ou o primeiro elemento de uma tupla contendo códigos de substituição.

Observação: há apenas uma coisa que não consigo descobrir como fazer: se você adicionar validação a uma entrada e o usuário selecionar uma parte do texto e digitar um novo valor, não há como capturar o valor original e redefinir a entrada. Aqui está um exemplo

  1. A entrada é projetada para aceitar apenas inteiros implementando 'validatecommand'
  2. O usuário digita 1234567
  3. O usuário seleciona '345' e pressiona 'j'. Isso é registrado como duas ações: exclusão de '345' e inserção de 'j'. Tkinter ignora a exclusão e atua apenas na inserção de 'j'. 'validatecommand' retorna False, e os valores passados ​​para a função 'invalidcommand' são os seguintes:% d = 1,% i = 2,% P = 12j67,% s = 1267,% S = j
  4. Se o código não implementar uma função 'comando inválido', a função 'comando validado' rejeitará o 'j' e o resultado será 1267. Se o código implementar uma função 'comando inválido', não há como recuperar o 1234567 original .
orionrobert
fonte
3

Esta é uma maneira simples de validar o valor de entrada, que permite ao usuário inserir apenas 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

PS: Este exemplo pode ser muito útil para criar um aplicativo como o calc.

Demian Wolf
fonte
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
fonte
2
Olá, bem-vindo ao Stack Overflow. Respostas "somente código" são desaprovadas, especialmente ao responder uma pergunta que já tem muitas respostas. Certifique-se de adicionar algumas dicas adicionais sobre por que a resposta que você está fornecendo é de alguma forma substantiva e não simplesmente ecoando o que já foi examinado pelo autor original.
chb
1
@Demian Wolf Gostei da sua versão melhorada da resposta original, mas tive que revertê-la. Por favor, considere postá-la como uma resposta sua (você pode encontrá-la no histórico de revisões ).
Marc.2377
1

Respondendo ao problema de orionrobert de lidar com validação simples mediante substituições de texto por meio da seleção, em vez de exclusões ou inserções separadas:

Uma substituição do texto selecionado é processada como uma exclusão seguida por uma inserção. Isso pode levar a problemas, por exemplo, quando a exclusão deve mover o cursor para a esquerda, enquanto uma substituição deve mover o cursor para a direita. Felizmente, esses dois processos são executados imediatamente um após o outro. Portanto, podemos diferenciar entre uma exclusão por si só e uma exclusão seguida diretamente por uma inserção devido a uma substituição, porque a última não altera o sinalizador de inatividade entre exclusão e inserção.

Isso é explorado usando um substitutionFlag e um Widget.after_idle(). after_idle()executa a função lambda no final da fila 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

É claro que, após uma substituição, ao validar a parte de exclusão, ainda não se saberá se uma inserção virá. Felizmente no entanto, com: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), nós podemos conseguir mais comportamento desejado retrospectivamente (já que a combinação de sua nova substitutionFlag com uma inserção é um novo evento único e final.

Stendert
fonte