Интерактивная проверка содержимого виджета Entry в tkinter
Какой метод рекомендуется для интерактивной проверки контента в tkinter? Entry
виджет?
Я прочитал посты об использовании validate=True
а также validatecommand=command
и кажется, что эти функции ограничены тем фактом, что они очищаются, если validatecommand
команда обновляет Entry
значение виджета.
Учитывая это поведение, мы должны связать KeyPress
, Cut
, а также Paste
события и следить / обновлять наши Entry
Значение виджета через эти события? (И другие связанные события, которые я мог пропустить?)
Или мы должны полностью забыть интерактивную проверку и выполнять проверку только на FocusOut
События?
10 ответов
Правильный ответ, используйте validatecommand
атрибут виджета. К сожалению, эта особенность недостаточно документирована в мире Tkinter, хотя в мире Tk она достаточно документирована. Несмотря на то, что он плохо документирован, в нем есть все необходимое для проверки, не прибегая к привязкам или отслеживанию переменных, а также не изменяя виджет в рамках процедуры проверки.
Хитрость заключается в том, что вы можете заставить Tkinter передавать специальные значения в вашу команду validate. Эти значения дают вам всю информацию, которую вам нужно знать, чтобы решить, являются ли данные действительными или нет: значение до редактирования, значение после редактирования, если редактирование является действительным, и несколько других битов информации. Однако, чтобы использовать их, вам нужно сделать немного вуду, чтобы передать эту информацию вашей команде проверки.
Примечание: важно, чтобы команда проверки вернула либо 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()
Изучив и экспериментировав с кодом Брайана, я подготовил минимальную версию проверки ввода. Следующий код создаст поле ввода и будет принимать только цифры.
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 и с удовольствием приму любые комментарии и предложения.
Использовать 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()
Ответ Брайана верен, однако никто не упомянул атрибут "invalidcommand" виджета tkinter.
Хорошее объяснение здесь: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html
Копия текста / вставка в случае неработающей ссылки
Виджет Entry также поддерживает параметр invalidcommand, который определяет функцию обратного вызова, которая вызывается всякий раз, когда validatecommand возвращает False. Эта команда может изменить текст в виджете с помощью метода.set() в связанной текстовой переменной виджета. Настройка этой опции работает так же, как установка команды validate. Вы должны использовать метод.register(), чтобы обернуть вашу функцию Python; этот метод возвращает имя обернутой функции в виде строки. Затем вы передадите в качестве значения опции invalidcommand либо эту строку, либо первый элемент кортежа, содержащий коды подстановки.
Примечание: есть только одна вещь, которую я не могу понять, как это сделать: если вы добавляете проверку к записи, а пользователь выбирает часть текста и вводит новое значение, то нет способа зафиксировать исходное значение и сбросить его. Вход. Вот пример
- Запись предназначена для приема только целых чисел путем реализации 'validatecommand'
- Пользователь вводит 1234567
- Пользователь выбирает "345" и нажимает "J". Это зарегистрировано как два действия: удаление "345" и вставка "j". Tkinter игнорирует удаление и действует только на вставку 'j'. 'validatecommand' возвращает False, и значения, передаваемые в функцию 'invalidcommand', следующие: %d=1, %i=2, %P=12j67, %s=1267, %S=j
- Если код не реализует функцию 'invalidcommand', функция 'validatecommand' отклонит 'j', и результат будет 1267. Если код действительно реализует функцию 'invalidcommand', восстановить исходную 1234567 невозможно.,
Вот простой способ проверить значение ввода, который позволяет пользователю вводить только цифры:
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.
Изучая ответ Брайана Оукли, что-то подсказывало мне, что может быть найдено гораздо более общее решение. В следующем примере представлено перечисление режима, словарь типов и функция настройки для проверки. Смотрите строку 48 для примера использования и демонстрации его простоты.
#! /usr/bin/env python3
# https://stackru.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 __name__ == '__main__':
Example.main()
Вот улучшенная версия @Steven Rumbalski в ответ на тестирующих
Версия ниже помещает все в
try:
from tkinter import *
except ImportError:
from Tkinter import * # Python 2
class ValidateFloatVar(StringVar):
"""StringVar subclass that only allows valid float values to be put in it."""
def __init__(self, master=None, value=None, name=None):
StringVar.__init__(self, master, value, name)
self._old_value = self.get()
self.trace('w', self._validate)
def _validate(self, *_):
new_value = self.get()
try:
new_value == '' or float(new_value)
self._old_value = new_value
except ValueError:
StringVar.set(self, self._old_value)
root = Tk()
ent = Entry(root, textvariable=ValidateFloatVar(value=42.0))
ent.pack()
ent.focus_set()
ent.icursor(END)
root.mainloop()
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
Этот код может помочь, если вы хотите установить как только цифры, так и максимальное количество символов.
from tkinter import *
root = Tk()
def validate(P):
if len(P) == 0 or len(P) <= 10 and P.isdigit(): # 10 characters
return True
else:
return False
ent = Entry(root, validate="key", validatecommand=(root.register(validate), '%P'))
ent.pack()
root.mainloop()
Отвечая на проблему Орионроберта о том, как справиться с простой проверкой при замене текста путем выделения вместо отдельных удалений или вставок:
Подстановка выделенного текста обрабатывается как удаление с последующей вставкой. Это может привести к проблемам, например, когда удаление должно переместить курсор влево, а замена должна переместить курсор вправо. К счастью, эти два процесса выполняются сразу после друг друга. Следовательно, мы можем провести различие между удалением само по себе и удалением, за которым непосредственно следует вставка из-за замены, поскольку последняя не имеет времени простоя между удалением и вставкой.
Это эксплуатируется с использованием 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 со вставкой является новым уникальным и окончательным событием.