Интерактивная проверка содержимого виджета Entry в tkinter

85

Каков рекомендуемый метод интерактивной проверки содержимого в Entryвиджете tkinter ?

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

Учитывая такое поведение, мы должны связать на KeyPress, Cutи Pasteсобытий и монитор / обновить наше Entryзначение виджета через эти события? (И другие связанные события, которые я мог пропустить?)

Или мы должны вообще забыть об интерактивной проверке и проверять ее только на FocusOutсобытиях?

Малькольм
источник

Ответы:

221

Правильный ответ - используйте 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.

Брайан Окли
источник
16
Это правильный способ сделать это. Он решает проблемы, которые я обнаружил, когда попытался заставить jmeyer10 ответить. Этот пример предоставляет лучшую документацию для проверки по сравнению с тем, что я могу найти в другом месте. Хотел бы я дать за это 5 голосов.
Steven Rumbalski
3
ВОТ ЭТО ДА! Я согласен со Стивеном - такой ответ заслуживает более одного голоса. Вам следует написать книгу о Tkinter (и вы уже опубликовали достаточно решений, чтобы сделать из нее многотомную серию). Спасибо!!!
Малькольм
2
Спасибо за пример. Стоит отметить, что команда validate ДОЛЖНА возвращать логическое значение (только True и False). В противном случае проверка будет удалена.
Dave Bacher
3
Думаю, эту страницу следует вывести на первый план.
Правая нога
4
"серьезно недокументирован в мире Tkinter". LOL - как почти весь остальной мир Tkiinter.
Мартино
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, и я с радостью приму любые комментарии / предложения.

user1683793
источник
1
Обычно люди используют entry.configure(validatecommand=...)и пишут test_valвместо testVal, но это хороший и простой пример.
wizzwizz4
10

Используйте 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 ().
Малькольм
есть идеи, почему я мог получить эту ошибку? «NameError: имя 'validate' не определено»
Армен Саноян
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 () для связанной текстовой переменной виджета. Настройка этого параметра работает так же, как настройка команды validate. Вы должны использовать метод .register (), чтобы обернуть вашу функцию Python; этот метод возвращает имя обернутой функции в виде строки. Затем вы передадите в качестве значения параметра invalidcommand либо эту строку, либо как первый элемент кортежа, содержащего коды подстановки.

Примечание: есть только одна вещь, которую я не могу понять, как это сделать: если вы добавляете проверку к записи, а пользователь выбирает часть текста и вводит новое значение, нет возможности захватить исходное значение и сбросить Вход. Вот пример

  1. Запись предназначена для приема только целых чисел путем реализации команды validatecommand.
  2. Пользователь вводит 1234567
  3. Пользователь выбирает «345» и нажимает «j». Это регистрируется как два действия: удаление «345» и вставка «j». Tkinter игнорирует удаление и действует только при вставке '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

Ответ на проблему орионроберта простой проверки при текста посредством выделения вместо отдельных удалений или вставок:

Замена выделенного текста обрабатывается как удаление с последующей вставкой. Это может привести к проблемам, например, когда при удалении курсор должен перемещаться влево, а при подстановке - вправо. К счастью, эти два процесса выполняются сразу один за другим. Следовательно, мы можем различать удаление само по себе и удаление, за которым непосредственно следует вставка из-за замены, поскольку последняя не меняет флаг простоя между удалением и вставкой.

Это используется с помощью substitutionFlag и файла 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 с прошивкой является новым уникальным и завершающим событием.

Stendert
источник