如何确保 ttk.Entry 的无效状态在失去焦点时不被清除?
How can I ensure my ttk.Entry's invalid state isn't cleared when it loses focus?
我想在内容更改时设置或清除 ttk.Entry
的 invalid
状态标志。为此,我将 StringVar
连接到条目,并在 trace()
回调中调用 state(['valid'])
或 state(['!invalid'])
。该标志由我的回调正确设置,但随后,只要焦点离开文本条目,它就会被清除!我该如何避免或解决这个问题?
我想设置或清除标志,因为我可以根据状态标志更改视觉样式。我不想禁止用户输入任何无效的内容;我希望他们可以自由地输入他们想要的任何内容,并立即查看它是否有效。我想专门使用 invalid
标志,而不是 alternate
标志,不仅因为 invalid
是更合乎逻辑的选择,而且因为我已经在使用 alternate
标记其他东西。
我没有使用此小部件的内置验证功能,因为根据 the Tk docs,如果我在编辑文本时调用验证命令(-validate
等于 'keys'
或 'all'
),
The entry will be prevalidated prior to each edit ... If prevalidation fails, the edit is rejected.
就像我之前说的,我不想那样。我想要 -validate
等于 'none'
应该做的事情:
validation will only occur when specifically requested by the validate
widget command.
太好了,所以理论上我所要做的就是永远不要调用 validate()
。不幸的是,invalid
标志无论如何都会被清除。我可以在 Python 的交互模式下重现这种意外和不需要的行为:
>>> import tkinter as tk
>>> from tkinter import ttk
>>> win = tk.Tk()
>>> entry = ttk.Entry(win)
>>> entry.pack()
>>> entry['validate']
'none'
>>> entry.state()
()
>>> entry.state(['invalid'])
('!invalid',)
>>> entry.state()
('invalid',)
到目前为止,还不错。 (我在这个例子中使用了 Python 3,但是我得到了与 Python 2 相同的结果。)现在我在输入框之间来回切换焦点,并且:
>>> entry.state()
()
当 -validate
是 'none'
而不是 'focus'
或 'all'
时,为什么它被清除了?为了我的目的,我可以做些什么来使用 invalid
状态?
我在 Python 3.4.2 和 2.7.9 上看到了相同的行为,使用 Tcl/Tk 版本 8.6,在 Linux。
这是您的问题的解决方案。您可能希望根据您的需要对其进行调整,但它可能会给您一个想法:
import tkinter as tk
from tkinter import ttk
class ValidatingEntry(ttk.Entry):
COLOR_VALID = "#99ff99"
COLOR_INVALID="#ff9999"
def __init__(self, master, *args, **kwargs):
self.stringVar = tk.StringVar()
tk.Entry.__init__(self, master, *args, textvariable = self.stringVar, **kwargs)
self.validatingFunction = None
self.bind("<FocusOut>",self.validation)
def validation(self, event):
if self.validatingFunction != None:
if self.validatingFunction():
self['bg']=ValidatingEntry.COLOR_VALID
else:
self['bg']=ValidatingEntry.COLOR_INVALID
else:
print("no evaluation possible for the content of this entry")
if __name__ == "__main__":
app = tk.Tk()
entry = ValidatingEntry(app)
entry.validatingFunction = lambda : 'foo' in entry.stringVar.get()
entry.pack()
entry2 = ValidatingEntry(app)
entry2.validatingFunction = lambda : 'bar' in entry2.stringVar.get()
entry2.pack()
app.mainloop()
可能可以修改 class 以获取模式参数并使用它来检查 StringVar 内容是否与模式匹配,例如正则表达式。但这不再与传统知识相关。
希望对您有所帮助。
亚瑟.
将您自己的绑定添加到 <FocusOut>
,它会调用您的验证函数并重置状态。
这是一个完整的工作示例。如果条目小部件包含单词 "invalid",状态将更改为 "invalid"。然后您可以单击小部件以查看状态仍然无效:
try:
import Tkinter as tk
import ttk
except ImportError:
import tkinter as tk
from tkinter import ttk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
# give invalid entries a red background
style = ttk.Style()
style.map("TEntry", background=[('invalid', "red")])
self.entryVar = tk.StringVar()
self.entry = ttk.Entry(self, textvariable=self.entryVar)
# this label will show the current state, updated
# every second.
self.label = tk.Label(self, anchor="w")
self.after_idle(self.updateLabel)
# layout the widgets
self.entry.pack(side="top", fill="x")
self.label.pack(side="bottom", fill="x")
# add trace on the variable to do custom validation
self.entryVar.trace("w", self.validate)
# set up bindings to also do the validation when we gain
# or lose focus
self.entry.bind("<FocusIn>", self.validate)
self.entry.bind("<FocusOut>", self.validate)
def updateLabel(self):
'''Display the current entry widget state'''
state = str(self.entry.state())
self.label.configure(text=state)
self.after(1000, self.updateLabel)
def validate(self, *args):
'''Validate the widget contents'''
value = self.entryVar.get()
if "invalid" in value:
self.entry.state(["invalid"])
else:
self.entry.state(["!invalid"])
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
为什么会这样
在 Tk 源文件 generic/ttk/ttkEntry.c
中,EntryRevalidate()
在焦点事件上总是 运行。这调用 EntryValidateChange()
,当它注意到焦点事件的验证是 "turned off"、returns 时,结果表明当前值是有效的 EntryRevalidate()
,相应地清除了 invalid
标志。
因此,正如目前实施的那样,invalid
标志不会通过焦点事件持续存在。 "No revalidation" 真正的意思是 "instead of doing revalidation, clear the invalid
flag".
一个解决方案
如果你打不过他们,那就加入他们吧。将 validate='focus'
与 validatecommand
一起使用 returns 无论文本是否有效,就好像您一直想在焦点事件上检查它一样。并在真正发生变化时继续设置 invalid
标志。例如:
class MyEntry(ttk.Entry):
def __init__(self, master):
self.var = tk.StringVar(master)
super().__init__(master,
textvariable=self.var,
validate='focus',
validatecommand=self.is_valid)
self.var.trace('w', self.revalidate)
self.revalidate()
def revalidate(self, *args):
self.state(['!invalid' if self.is_valid()
else 'invalid'])
def is_valid(self, *args):
# ... return if current text is valid ...
另一个(类似的)解决方案
您可以改为简单地在焦点事件中保留状态标志:
class ValidationDisabledEntry(ttk.Entry):
def __init__(self, *args, **kwargs):
super().__init__(
*args,
validate='focus',
validatecommand=lambda *a: 'invalid' not in self.state(),
**kwargs)
然后随时设置或清除 invalid
标志,而不用担心焦点事件会清除它。如上所述设置 validate
和 validatecommand
应该(并且似乎)实现我认为 validate='none'
会得到我的行为。
我想在内容更改时设置或清除 ttk.Entry
的 invalid
状态标志。为此,我将 StringVar
连接到条目,并在 trace()
回调中调用 state(['valid'])
或 state(['!invalid'])
。该标志由我的回调正确设置,但随后,只要焦点离开文本条目,它就会被清除!我该如何避免或解决这个问题?
我想设置或清除标志,因为我可以根据状态标志更改视觉样式。我不想禁止用户输入任何无效的内容;我希望他们可以自由地输入他们想要的任何内容,并立即查看它是否有效。我想专门使用 invalid
标志,而不是 alternate
标志,不仅因为 invalid
是更合乎逻辑的选择,而且因为我已经在使用 alternate
标记其他东西。
我没有使用此小部件的内置验证功能,因为根据 the Tk docs,如果我在编辑文本时调用验证命令(-validate
等于 'keys'
或 'all'
),
The entry will be prevalidated prior to each edit ... If prevalidation fails, the edit is rejected.
就像我之前说的,我不想那样。我想要 -validate
等于 'none'
应该做的事情:
validation will only occur when specifically requested by the
validate
widget command.
太好了,所以理论上我所要做的就是永远不要调用 validate()
。不幸的是,invalid
标志无论如何都会被清除。我可以在 Python 的交互模式下重现这种意外和不需要的行为:
>>> import tkinter as tk
>>> from tkinter import ttk
>>> win = tk.Tk()
>>> entry = ttk.Entry(win)
>>> entry.pack()
>>> entry['validate']
'none'
>>> entry.state()
()
>>> entry.state(['invalid'])
('!invalid',)
>>> entry.state()
('invalid',)
到目前为止,还不错。 (我在这个例子中使用了 Python 3,但是我得到了与 Python 2 相同的结果。)现在我在输入框之间来回切换焦点,并且:
>>> entry.state()
()
当 -validate
是 'none'
而不是 'focus'
或 'all'
时,为什么它被清除了?为了我的目的,我可以做些什么来使用 invalid
状态?
我在 Python 3.4.2 和 2.7.9 上看到了相同的行为,使用 Tcl/Tk 版本 8.6,在 Linux。
这是您的问题的解决方案。您可能希望根据您的需要对其进行调整,但它可能会给您一个想法:
import tkinter as tk
from tkinter import ttk
class ValidatingEntry(ttk.Entry):
COLOR_VALID = "#99ff99"
COLOR_INVALID="#ff9999"
def __init__(self, master, *args, **kwargs):
self.stringVar = tk.StringVar()
tk.Entry.__init__(self, master, *args, textvariable = self.stringVar, **kwargs)
self.validatingFunction = None
self.bind("<FocusOut>",self.validation)
def validation(self, event):
if self.validatingFunction != None:
if self.validatingFunction():
self['bg']=ValidatingEntry.COLOR_VALID
else:
self['bg']=ValidatingEntry.COLOR_INVALID
else:
print("no evaluation possible for the content of this entry")
if __name__ == "__main__":
app = tk.Tk()
entry = ValidatingEntry(app)
entry.validatingFunction = lambda : 'foo' in entry.stringVar.get()
entry.pack()
entry2 = ValidatingEntry(app)
entry2.validatingFunction = lambda : 'bar' in entry2.stringVar.get()
entry2.pack()
app.mainloop()
可能可以修改 class 以获取模式参数并使用它来检查 StringVar 内容是否与模式匹配,例如正则表达式。但这不再与传统知识相关。
希望对您有所帮助。 亚瑟.
将您自己的绑定添加到 <FocusOut>
,它会调用您的验证函数并重置状态。
这是一个完整的工作示例。如果条目小部件包含单词 "invalid",状态将更改为 "invalid"。然后您可以单击小部件以查看状态仍然无效:
try:
import Tkinter as tk
import ttk
except ImportError:
import tkinter as tk
from tkinter import ttk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
# give invalid entries a red background
style = ttk.Style()
style.map("TEntry", background=[('invalid', "red")])
self.entryVar = tk.StringVar()
self.entry = ttk.Entry(self, textvariable=self.entryVar)
# this label will show the current state, updated
# every second.
self.label = tk.Label(self, anchor="w")
self.after_idle(self.updateLabel)
# layout the widgets
self.entry.pack(side="top", fill="x")
self.label.pack(side="bottom", fill="x")
# add trace on the variable to do custom validation
self.entryVar.trace("w", self.validate)
# set up bindings to also do the validation when we gain
# or lose focus
self.entry.bind("<FocusIn>", self.validate)
self.entry.bind("<FocusOut>", self.validate)
def updateLabel(self):
'''Display the current entry widget state'''
state = str(self.entry.state())
self.label.configure(text=state)
self.after(1000, self.updateLabel)
def validate(self, *args):
'''Validate the widget contents'''
value = self.entryVar.get()
if "invalid" in value:
self.entry.state(["invalid"])
else:
self.entry.state(["!invalid"])
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
为什么会这样
在 Tk 源文件 generic/ttk/ttkEntry.c
中,EntryRevalidate()
在焦点事件上总是 运行。这调用 EntryValidateChange()
,当它注意到焦点事件的验证是 "turned off"、returns 时,结果表明当前值是有效的 EntryRevalidate()
,相应地清除了 invalid
标志。
因此,正如目前实施的那样,invalid
标志不会通过焦点事件持续存在。 "No revalidation" 真正的意思是 "instead of doing revalidation, clear the invalid
flag".
一个解决方案
如果你打不过他们,那就加入他们吧。将 validate='focus'
与 validatecommand
一起使用 returns 无论文本是否有效,就好像您一直想在焦点事件上检查它一样。并在真正发生变化时继续设置 invalid
标志。例如:
class MyEntry(ttk.Entry):
def __init__(self, master):
self.var = tk.StringVar(master)
super().__init__(master,
textvariable=self.var,
validate='focus',
validatecommand=self.is_valid)
self.var.trace('w', self.revalidate)
self.revalidate()
def revalidate(self, *args):
self.state(['!invalid' if self.is_valid()
else 'invalid'])
def is_valid(self, *args):
# ... return if current text is valid ...
另一个(类似的)解决方案
您可以改为简单地在焦点事件中保留状态标志:
class ValidationDisabledEntry(ttk.Entry):
def __init__(self, *args, **kwargs):
super().__init__(
*args,
validate='focus',
validatecommand=lambda *a: 'invalid' not in self.state(),
**kwargs)
然后随时设置或清除 invalid
标志,而不用担心焦点事件会清除它。如上所述设置 validate
和 validatecommand
应该(并且似乎)实现我认为 validate='none'
会得到我的行为。