Interaktives Überprüfen des Eintrags-Widget-Inhalts in tkinter

82

Was ist die empfohlene Technik zum interaktiven Überprüfen von Inhalten in einem tkinter- EntryWidget?

Ich habe die Beiträge über die Verwendung von validate=Trueund gelesen validatecommand=command, und es scheint, dass diese Funktionen durch die Tatsache eingeschränkt sind, dass sie gelöscht werden, wenn der validatecommandBefehl das aktualisiertEntry Wert Widgets .

In Anbetracht dieses Verhalten, sollten wir binden an die KeyPress, Cutund PasteEreignisse und Monitor / aktualisieren unsereEntry Widgets Wert durch diese Ereignisse? (Und andere verwandte Ereignisse, die ich möglicherweise verpasst habe?)

Oder sollten wir die interaktive Validierung ganz vergessen und nur bei FocusOutEreignissen validieren ?

Malcolm
quelle

Antworten:

208

Die richtige Antwort lautet: Verwenden Sie das validatecommandAttribut des Widgets. Leider ist diese Funktion in der Tkinter-Welt stark unterdokumentiert, obwohl sie in der Tkinter-Welt ausreichend dokumentiert ist. Obwohl es nicht gut dokumentiert ist, bietet es alles, was Sie für die Validierung benötigen, ohne auf Bindungen oder Ablaufverfolgungsvariablen zurückgreifen oder das Widget innerhalb des Validierungsverfahrens ändern zu müssen.

Der Trick besteht darin, zu wissen, dass Tkinter spezielle Werte an Ihren Validierungsbefehl übergeben kann. Diese Werte geben Ihnen alle Informationen, die Sie wissen müssen, um zu entscheiden, ob die Daten gültig sind oder nicht: den Wert vor der Bearbeitung, den Wert nach der Bearbeitung, wenn die Bearbeitung gültig ist, und mehrere andere Informationen. Um diese zu verwenden, müssen Sie jedoch ein wenig Voodoo machen, um diese Informationen an Ihren Validierungsbefehl zu übergeben.

Hinweis: Es ist wichtig, dass der Validierungsbefehl entweder Trueoder zurückgibt False. Alles andere führt dazu, dass die Validierung für das Widget deaktiviert wird.

Hier ist ein Beispiel, das nur Kleinbuchstaben zulässt (und alle diese funky Werte druckt):

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

Weitere Informationen darüber, was beim Aufrufen der registerMethode unter der Haube passiert, finden Sie unter Eingabeüberprüfung tkinter

Bryan Oakley
quelle
14
Dies ist der richtige Weg. Es behebt die Probleme, die ich gefunden habe, als ich versucht habe, die Antwort von jmeyer10 zum Laufen zu bringen. Dieses eine Beispiel bietet eine überlegene Dokumentation zur Validierung im Vergleich zu dem, was ich anderswo finden kann. Ich wünschte, ich könnte diese 5 Stimmen geben.
Steven Rumbalski
3
BEEINDRUCKEND! Ich stimme Steven zu - dies ist die Art von Antwort, die mehr als eine Stimme verdient. Sie sollten ein Buch über Tkinter schreiben (und Sie haben bereits genügend Lösungen veröffentlicht, um daraus eine mehrbändige Serie zu machen). Danke dir!!!
Malcolm
2
Danke für das Beispiel. Es ist erwähnenswert, dass der Befehl validate einen Booleschen Wert zurückgeben MUSS (nur True und False). Wenn nicht, wird die Validierung entfernt.
Dave Bacher
3
Ich denke, diese Seite sollte in den Vordergrund gerückt werden.
Rechtes Bein
4
"In der Tkinter-Welt stark unterdokumentiert". LOL - wie fast der ganze Rest der Tkiinter-Welt.
Martineau
21

Nachdem ich Bryans Code studiert und experimentiert hatte, erstellte ich eine minimale Version der Eingabevalidierung. Der folgende Code öffnet ein Eingabefeld und akzeptiert nur numerische Ziffern.

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

Vielleicht sollte ich hinzufügen, dass ich noch Python lerne und gerne alle Kommentare / Vorschläge akzeptiere.

user1683793
quelle
1
Im Allgemeinen verwenden entry.configure(validatecommand=...)und schreiben die Leute test_valstatt testVal, aber dies ist ein gutes, einfaches Beispiel.
wizzwizz4
10

Verwenden Sie a Tkinter.StringVar, um den Wert des Eintrags-Widgets zu verfolgen. Sie können den Wert von überprüfen, StringVarindem Sie a einstellentrace darauf setzen.

Hier ist ein kurzes Arbeitsprogramm, das nur gültige Floats im Eintrags-Widget akzeptiert.

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
quelle
1
Danke für deinen Beitrag. Ich habe es genossen, die verwendete Tkinter StringVar .trace () -Methode zu sehen.
Malcolm
4

Während ich Bryan Oakleys Antwort studierte, sagte mir etwas, dass eine weitaus allgemeinere Lösung entwickelt werden könnte. Im folgenden Beispiel werden eine Modusaufzählung, ein Typwörterbuch und eine Setup-Funktion zu Validierungszwecken vorgestellt. In Zeile 48 finden Sie beispielsweise die Verwendung und eine Demonstration der Einfachheit.

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

Bryans Antwort ist richtig, aber niemand erwähnte das Attribut 'invalidcommand' des tkinter-Widgets.

Eine gute Erklärung finden Sie hier: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Text kopiert / eingefügt bei defektem Link

Das Eintrags-Widget unterstützt auch eine ungültige Befehlsoption, die eine Rückruffunktion angibt, die aufgerufen wird, wenn der validate-Befehl False zurückgibt. Dieser Befehl kann den Text im Widget mithilfe der .set () -Methode für die zugehörige Textvariable des Widgets ändern. Das Einrichten dieser Option funktioniert genauso wie das Einrichten des Befehls validate. Sie müssen die .register () -Methode verwenden, um Ihre Python-Funktion zu verpacken. Diese Methode gibt den Namen der umschlossenen Funktion als Zeichenfolge zurück. Dann übergeben Sie als Wert der Option invalidcommand entweder diese Zeichenfolge oder als erstes Element eines Tupels, das Substitutionscodes enthält.

Hinweis: Es gibt nur eine Sache, die ich nicht herausfinden kann: Wenn Sie einem Eintrag eine Validierung hinzufügen und der Benutzer einen Teil des Textes auswählt und einen neuen Wert eingibt, gibt es keine Möglichkeit, den ursprünglichen Wert zu erfassen und zurückzusetzen der Eintritt. Hier ist ein Beispiel

  1. Der Eintrag akzeptiert nur Ganzzahlen, indem der Befehl 'validate' implementiert wird.
  2. Benutzer gibt 1234567 ein
  3. Der Benutzer wählt '345' und drückt 'j'. Dies wird als zwei Aktionen registriert: Löschen von '345' und Einfügen von 'j'. Tkinter ignoriert das Löschen und wirkt nur auf das Einfügen von 'j'. 'validatecommand' gibt False zurück und die an die Funktion 'invalidcommand' übergebenen Werte lauten wie folgt:% d = 1,% i = 2,% P = 12j67,% s = 1267,% S = j
  4. Wenn der Code keine 'invalidcommand'-Funktion implementiert, lehnt die' validatecommand'-Funktion das 'j' ab und das Ergebnis ist 1267. Wenn der Code eine 'invalidcommand'-Funktion implementiert, gibt es keine Möglichkeit, die ursprüngliche 1234567 wiederherzustellen .
Orionrobert
quelle
3

Hier ist eine einfache Möglichkeit, den Eingabewert zu validieren, bei der der Benutzer nur Ziffern eingeben kann:

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: Dieses Beispiel kann sehr nützlich sein, um eine App wie calc zu erstellen.

Demian Wolf
quelle
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
quelle
2
Hallo, willkommen bei Stack Overflow. "Nur-Code" -Antworten werden verpönt, insbesondere wenn eine Frage beantwortet wird, die bereits viele Antworten enthält. Bitte stellen Sie sicher, dass Sie zusätzliche Informationen darüber hinzufügen, warum die von Ihnen bereitgestellte Antwort irgendwie inhaltlich ist und nicht nur das widerspiegelt, was bereits vom Originalplakat überprüft wurde.
chb
1
@Demian Wolf Ich mochte Ihre verbesserte Version der ursprünglichen Antwort, aber ich musste sie zurücksetzen. Bitte erwägen Sie, es als eigene Antwort zu veröffentlichen (Sie finden es im Revisionsverlauf ).
März 2377
1

Antwort auf das Problem von orionrobert, sich mit der einfachen Validierung von Textersetzungen durch Auswahl zu befassen, anstatt separate Löschungen oder Einfügungen:

Eine Ersetzung des ausgewählten Textes wird als Löschung gefolgt von einer Einfügung verarbeitet. Dies kann beispielsweise zu Problemen führen, wenn beim Löschen der Cursor nach links bewegt werden soll, während beim Ersetzen der Cursor nach rechts bewegt werden soll. Glücklicherweise werden diese beiden Prozesse sofort ausgeführt nacheinander . Daher können wir zwischen einer Löschung für sich und einer Löschung unterscheiden, auf die unmittelbar eine Einfügung aufgrund einer Substitution folgt, da letztere das Leerlauf-Flag zwischen Löschung und Einfügung nicht geändert hat.

Dies wird mit einem SubstitutionFlag und einem ausgenutzt Widget.after_idle(). after_idle()führt die Lambda-Funktion am Ende der Ereigniswarteschlange aus:

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

Natürlich weiß man nach einer Ersetzung während der Validierung des Löschteils immer noch nicht, ob eine Einfügung folgen wird. Zum Glück jedoch mit: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), können wir am meisten gewünschtes Verhalten nachträglich erreichen (da die Kombination unserer neuen substitutionFlag mit einer Insertion ist eine neue , einzigartige und letzte Veranstaltung.

Stendert
quelle