Інтерактивна перевірка вмісту віджета Entry у tkinter


85

Який рекомендований прийом для інтерактивної перевірки вмісту у Entryвіджеті tkinter ?

Я прочитав повідомлення про використання validate=Trueта validatecommand=command, і, схоже, ці функції обмежені тим, що вони очищаються, якщо validatecommandкоманда оновлює значення Entryвіджета.

З огляду на таку поведінку, ми повинні пов'язати на KeyPress, Cutі Pasteподій і монітор / оновити наше Entryзначення віджета через ці події? (І інші пов’язані події, які я міг пропустити?)

Або ми повинні взагалі забути інтерактивну перевірку та перевіряти лише FocusOutподії?

Відповіді:


217

Правильна відповідь - використовувати validatecommandатрибут віджета. На жаль, ця функція сильно задокументована у світі Tkinter, хоча вона цілком достатньо задокументована у світі Tk. Незважаючи на те, що це не задокументовано добре, у ньому є все, що потрібно для перевірки, не вдаючись до прив'язок чи відстеження змінних, або модифікуючи віджет в рамках процедури перевірки.

Фокус полягає у тому, щоб знати, що ви можете дозволити Tkinter передати спеціальні значення до вашої команди перевірки. Ці значення дають вам всю інформацію, яку потрібно знати, щоб вирішити, чи дані є дійсними чи ні: значення до редагування, значення після редагування, якщо редагування є дійсним, та кілька інших бітів інформації. Однак, щоб використовувати їх, вам потрібно зробити трохи вуду, щоб ця інформація передалася вашій команді перевірки.

Примітка: важливо, щоб команда перевірки повертала Trueабо False. Що-небудь ще призведе до вимкнення перевірки для віджета.

Ось приклад, який дозволяє лише малі літери (і друкує всі ці прикольні значення):

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

Щоб отримати додаткові відомості про те, що відбувається під капотом, коли ви викликаєте registerметод, див. Перевірку вводу tkinter


15
Це правильний спосіб зробити це. У ньому розглядаються проблеми, які я виявив, коли намагався отримати відповідь jmeyer10. Цей один приклад забезпечує чудову документацію для перевірки порівняно з тим, що я можу знайти деінде. Я хотів би дати цим 5 голосів.
Стівен Румбальський

3
ОЦЕ ТАК! Я погоджуюся зі Стівеном - такий тип відповіді заслуговує не одного голосу. Вам слід написати книгу про Tkinter (і ви вже опублікували достатньо рішень, щоб зробити цю багатотомну серію). Дякую!!!
Малкольм

2
Дякую за приклад. Варто зазначити, що валідуюча команда ПОВИННА повернути логічне значення (лише True і False). Якщо ні, перевірку буде видалено.
Дейв Бачер,

3
Я думаю, що цю сторінку слід висувати на перший план.
права нога

4
"суто недокументований у світі Ткінтера". LOL - як майже весь інший світ Ткінтер.
martineau

21

Після вивчення та експериментів із кодом Брайана я створив мінімальну версію перевірки вхідних даних. У наведеному нижче коді буде розміщено поле Введення та прийматимуть лише цифрові цифри.

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

Можливо, мені слід додати, що я все ще вивчаю Python, і я із задоволенням прийму будь-які коментарі / пропозиції.


1
Зазвичай люди використовують entry.configure(validatecommand=...)і пишуть test_valзамість testVal, але це хороший, простий приклад.
wizzwizz4

10

Використовуйте a Tkinter.StringVarдля відстеження значення віджета Entry. Ви можете перевірити значення параметра StringVar, встановивши traceна ньому значення.

Ось коротка робоча програма, яка приймає лише допустимі плаваючі елементи у віджеті 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()

1
Дякуємо за ваш допис. Мені сподобалося бачити метод Tkinter StringVar .trace ().
Малкольм

4

Вивчаючи відповідь Брайана Оклі , щось мені підказало, що можна розробити набагато більш загальне рішення. У наступному прикладі представлено перелік режимів, словник типів та функцію налаштування для цілей перевірки. Див. Рядок 48 для прикладу використання та демонстрації його простоти.

#! /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()

4

Відповідь Брайана правильна, проте ніхто не згадав атрибут 'invalidcommand' віджета tkinter.

Хороше пояснення тут: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Текстова копія / вставка у разі непрацюючого посилання

Віджет Entry також підтримує параметр invalidcommand, який визначає функцію зворотного виклику, що викликається щоразу, коли validatecommand повертає False. Ця команда може модифікувати текст у віджеті, використовуючи метод .set () у пов'язаній із змінною тексту віджета. Налаштування цієї опції працює так само, як налаштування команди перевірки. Ви повинні використовувати метод .register (), щоб обернути вашу функцію Python; цей метод повертає ім'я обгорнутої функції у вигляді рядка. Тоді ви передасте як значення параметра invalidcommand або той рядок, або як перший елемент кортежу, що містить коди підстановки.

Примітка: Існує лише одне, чого я не можу зрозуміти, як це зробити: Якщо ви додаєте перевірку до запису, а користувач виділяє частину тексту та вводить нове значення, неможливо захопити початкове значення та скинути запис. Ось приклад

  1. Запис призначений лише для прийняття цілих чисел, реалізуючи "перевірити команду"
  2. Користувач вводить 1234567
  3. Користувач вибирає "345" і натискає "j". Це реєструється як дві дії: видалення "345" та вставка "j". Ткінтер ігнорує видалення і діє лише на вставку "j". 'validatecommand' повертає False, а значення, передані функції 'invalidcommand', є такими:% d = 1,% i = 2,% P = 12j67,% s = 1267,% S = j
  4. Якщо код не реалізує функцію 'invalidcommand', функція 'validatecommand' відкине 'j', і результат буде 1267. Якщо код дійсно реалізує функцію 'invalidcommand', немає можливості відновити початковий 1234567 .

3

Ось простий спосіб перевірити значення входу, який дозволяє користувачеві вводити лише цифри:

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: Цей приклад може бути дуже корисним для створення такої програми, як calc.


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

2
Привіт, ласкаво просимо до Stack Overflow. Відповіді "лише для коду" не приймаються, особливо коли відповідають на питання, на які вже є багато відповідей. Будь ласка, не забудьте додати трохи додаткового розуміння того, чому відповідь, яку ви надаєте, є якось суттєвою, а не просто повторює те, що вже перевірено оригінальним плакатом.
chb

1
@Demian Wolf Мені сподобалась ваша вдосконалена версія оригінальної відповіді, але мені довелося відкотити її назад. Будь ласка, розгляньте можливість розміщення його як власної відповіді (ви можете знайти це в історії редагувань ).
Marc.2377,

1

Відповідаючи на проблему orionrobert, яка стосується простої перевірки при заміні тексту шляхом виділення, замість окремих видалень або вставок:

Заміна виділеного тексту обробляється як видалення з подальшою вставкою. Це може призвести до проблем, наприклад, коли при видаленні слід перемістити курсор вліво, а підстановці - курсор вправо. На щастя, ці два процеси виконуються одразу один за одним. Отже, ми можемо розрізнити видалення само по собі та видалення, за яким слідує вставка через заміну, оскільки остання не змінює прапор простою між видаленням та вставкою.

Це експлуатується за допомогою заміни Flag і a Widget.after_idle(). after_idle()виконує лямбда-функцію в кінці черги подій:

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

Звичайно, після заміни, під час перевірки частини видалення, все одно не буде знати, чи буде вставка слідувати. На щастя , однак, з: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), ми можемо досягти найбільш бажаної поведінки ретроспективно (так як поєднання нашого нового substitutionFlag з прошивкою є новим унікальним і завершальною подією.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.