import tkinter as tk
import locale
from tkinter import font as tkFont
class DebouncedNumericEntry(tk.Entry):
def __init__(self, master=None, debounce_delay=500, warning_duration=3000, allow_decimal=True, allow_negative=True, **kwargs):
"""
初始化数字输入框组件
:param master: 父组件
:param debounce_delay: 防抖延迟时间(毫秒)
:param warning_duration: 警告标签显示时长(毫秒)
:param allow_decimal: 是否允许小数点
:param allow_negative: 是否允许负号
:param kwargs: 其他传递给Entry的参数
"""
super().__init__(master, **kwargs)
self.debounce_delay = debounce_delay
self.warning_duration = warning_duration
self.allow_decimal = allow_decimal
self.allow_negative = allow_negative
self.after_id = None
if "textvariable" in kwargs:
self.textvariable = kwargs["textvariable"]
self._check_and_clean()
self.textvariable.trace_add("write", self._on_textvariable_change)
self.bind("<KeyRelease>", self._on_key_release)
self.bind("<Configure>", self._on_resize)
self.default_font = tkFont.nametofont("TkDefaultFont")
self.warning_label = tkFont.Font(family=self.default_font["family"], size=self.default_font["size"] - 1, weight=self.default_font['weight'])
self.warning_label = tk.Label(self, text="Only numbers, dot or minus!", font=self.warning_label)
self.warning_label.config(bg="yellow", fg="red")
if locale.getlocale()[0].startswith('Chinese'):
if allow_decimal and allow_negative:
self.warning_label.config(text='仅支持数字、小数点或负号')
elif allow_decimal:
self.warning_label.config(text='仅支持数字或小数点')
elif allow_negative:
self.warning_label.config(text='仅支持数字或负号')
else:
self.warning_label.config(text='仅支持数字')
else:
if allow_decimal and allow_negative:
self.warning_label.config(text="Only numbers, dot or minus!")
elif allow_decimal:
self.warning_label.config(text="Only numbers or dot!")
elif allow_negative:
self.warning_label.config(text="Only numbers or minus!")
else:
self.warning_label.config(text="Only numbers!")
def _on_textvariable_change(self, *args):
"""
textvariable变化时触发的处理函数
"""
self._on_key_release()
def _on_key_release(self, event=None):
"""
键盘释放事件处理函数
"""
if self.after_id is not None:
self.after_cancel(self.after_id)
self.after_id = self.after(self.debounce_delay, self._check_and_clean)
def _check_and_clean(self):
"""
检查输入内容并清理非法字符
"""
content = self.get()
current_cursor_pos = self.index(tk.INSERT)
bfc = ''.join(filter(self.is_valid_char, content[:current_cursor_pos]))
p = len(self.clean_content(bfc))
filtered_content = ''.join(filter(self.is_valid_char, content))
filtered_content = self.clean_content(filtered_content)
if filtered_content != content:
self.delete(0, tk.END)
self.insert(0, filtered_content)
if hasattr(self, 'textvariable'):
self.textvariable.set(filtered_content)
self.icursor(p)
self._flash_warning_label()
self.after_id = None
def _flash_warning_label(self):
"""短暂显示警告标签并自动隐藏"""
if hasattr(self, 'warning_label'):
self.warning_label.place(x=self.winfo_width() - self.warning_label.winfo_width(), y=0)
self.warning_label.after(self.warning_duration, lambda: self.warning_label.place_forget())
def _on_resize(self, event=None):
self._update_warning_label_position()
def _update_warning_label_position(self):
if self.warning_label.winfo_ismapped():
self.warning_label.place_forget()
self.warning_label.place(x=self.winfo_width() - self.warning_label.winfo_width(), y=0)
self.warning_label.after(self.warning_duration, lambda: self.warning_label.place_forget())
def is_valid_char(self, char: str):
"""检查字符是否为数字、小数点或负号"""
if char.isdigit():
return True
if self.allow_decimal and char == '.':
return True
if self.allow_negative and char == '-':
return True
return False
def clean_content(self, content: str):
"""
清理多余的负号和小数点
- 负号只允许在首位
- 多个小数点只保留最左侧的小数点
"""
if not content:
return content
if self.allow_negative and content[0] == '-':
content = '-' + content[1:].replace('-', '')
else:
content = content.replace('-', '')
if self.allow_decimal and '.' in content:
parts = content.split('.')
content = parts[0] + '.' + ''.join(parts[1:])
else:
content = content.replace('.', '')
return content
if __name__ == "__main__":
root = tk.Tk()
root.geometry("400x200+400+400")
root.title("防抖数字输入框")
text = tk.StringVar(value='默认值是1')
entry = DebouncedNumericEntry(root, debounce_delay=500, warning_duration=500, textvariable=text)
entry.pack()
root.after(1000, lambda: entry.config(width=50))
root.after(2000, lambda: text.set("修改为2"))
root.mainloop()