摘要:单击 tkinter SpinBox 箭头会增加 GUI 中的数字,但对条目进行检查与关联的 StringVar() 会滞后于单击。
我创建了包含数字条目的复合小部件,这些小部件在提交条目更改时调用模型计算。我不想在每次更改条目时简单地调用模型,因为计算可能会很慢。因此,实际代码使用 <Tab>
上的绑定(bind), <Return>
等(从下面的示例代码中省略)并且仅在此类“提交”时调用模型。
我查看的 SpinBox 文档没有透露绑定(bind)到 SpinBox 本身的向上/向下箭头按钮的方法。因此,我有用于单击和释放鼠标按钮的绑定(bind)。单击并释放时,会将最后存储的值与当前 StringVar 值进行比较,如果它们不同,则更新存储的值并调用模型。涉及到的关键方法有bind_entry
, on_press
, on_release
, refresh
, entry_is_changed
,和save_entry
。 (on_press
和 on_release
中有注释掉的代码,这些代码允许在按下 SpinBox 箭头时刷新模型;保留它是为了使预期的最终行为清晰,但不需要复制错误) .
from tkinter import * # technically bad practice, but common
class SpinBoxFrame(Frame):
"""
A tkinter Frame that holds a labeled entry widget with added behavior.
EntryFrame will call the function (provided as 'model' in the arguments)
when a change in the entry's value is committed.
Arguments (in addition to standard Frame options):
name-- for widget label and introspection
array-- a 2D array ( list of lists, [[],[]]
coord-- the coordinate of the array to be read from/written to
from_, to, increment: SpinBox arguments (minimum and maximum values,
and incremental change on each arrow click)
"""
def __init__(self, parent=None, name='', model=None,
array=None, coord=(0, 0),
from_=0.00, to=100.00, increment=1,
**options):
Frame.__init__(self, parent, **options)
self.name = name
self.model = model
self.array = array
self.row, self.col = coord
self.spinbox_kwargs = {'from_': from_,
'to': to,
'increment': increment}
self.initialize()
self.add_label()
self.add_entry()
self.bind_entry()
self.validate_entry()
def initialize(self):
self.value_var = StringVar()
self.value = self.array[self.row][self.col]
self.value_var.set(self.value)
def add_label(self):
Label(self, text=self.name, bg='white', bd=0).pack(side=TOP)
def add_entry(self):
self.entry = Spinbox(self, width=7,
validate='key', # check for number on keypress
**self.spinbox_kwargs)
self.entry.pack(side=TOP, fill=X)
self.entry.config(textvariable=self.value_var)
def bind_entry(self):
self.entry.bind('<FocusOut>', lambda event: self.refresh())
self.entry.bind('<ButtonPress-1>', lambda event: self.on_press())
self.entry.bind('<ButtonRelease-1>', lambda event: self.on_release())
def refresh(self):
if self.entry_is_changed():
print('VALUE CHANGE')
self.save_entry()
print('Saved new ', self.name, ' value')
self.model()
def entry_is_changed(self):
print('Old value of ', self.name, ' was ', self.value)
print('Current value of ', self.name, ' is ',
float(self.value_var.get()))
return self.value != float(self.value_var.get())
def save_entry(self):
if not self.value_var.get(): # if entry left blank,
self.value_var.set(0.00) # fill it with zero
self.value = float(self.value_var.get())
self.array[self.row][self.col] = self.value
def on_press(self):
print('Button pressed')
# self.loop_refresh() # uncomment to enable real-time refreshing
def loop_refresh(self):
self.refresh()
self.button_held_job = self._root().after(50, self.loop_refresh)
def on_release(self):
print('Button released')
# uncomment if loop enabled in on_press()
# self._root().after_cancel(self.button_held_job)
self.refresh()
def validate_entry(self):
"""
The base EntryFrame class assumes the entry contents should be numerical
"""
# check on each keypress if new result will be a number
self.entry['validatecommand'] = (self.register(self.is_number), '%P')
# sound 'bell' if bad keypress
self.entry['invalidcommand'] = 'bell'
@staticmethod
def is_number(entry):
"""
tests to see if entry is acceptable (either empty, or able to be
converted to a float.)
"""
if not entry:
return True # Empty string: OK if entire entry deleted
try:
float(entry)
return True
except ValueError:
return False
if __name__ == '__main__':
dummy_array = [[1, 42], [0, 0]]
root = Tk()
class TestFrame(Frame):
"""Mimics a toolbar object that holds entry widgets and passes their
entries on to a model"""
def __init__(self, parent, **options):
Frame.__init__(self, parent, **options)
def call_model(self):
print('requesting calculation from the model, using:')
print(dummy_array)
mainwindow = TestFrame(root)
mainwindow.pack()
box1 = SpinBoxFrame(mainwindow, array=dummy_array, coord=(0, 0),
name='Box 1', model=mainwindow.call_model)
box1.pack(side=LEFT)
box2 = SpinBoxFrame(mainwindow, array=dummy_array, coord=(0, 1),
name='Box 2', model=mainwindow.call_model)
box2.pack(side=LEFT)
# workaround fix for Tk problems and mac mouse/trackpad:
while True:
try:
root.mainloop()
break
except UnicodeDecodeError:
pass
单击并释放 SpinBox 箭头可增加/减少 GUI 中的值,但检查与 StringVar 的比较表明没有变化。沿相同方向第二次单击同一箭头会导致更改,但更改为之前的值而不是当前的 GUI 值。因此,StringVar 检查始终落后于显示值 1。我想知道我的代码运行速度与 tkinter 主循环更新与 SpinBox 条目关联的 StringVar 对象的速度是否存在一些“竞争条件”。看起来像entry_is_changed
调用速度快于 StringVar 的更新速度。
屏幕截图显示了错误的性质。首先,通过单击,左侧小部件从 1 增加到 2,但值检查仍然表明当前 StringVar 仍然保留“1”。然后,右侧的小部件增加了两次。第一次从 42 增加到 43 后,没有看到任何值变化。第二次从 43 增加到 44 后,可以看到值 43 发生了变化。
最佳答案
我认为 <ButtonRelease-1>
事件触发器refresh
在变量值更新之前。为了避免这种情况,我在调用refresh
之前引入了一个小的延迟。在on_release
:
def on_release(self):
print('Button released')
# uncomment if loop enabled in on_press()
# self._root().after_cancel(self.button_held_job)
self.after(1, self.refresh)
至少在我的计算机上,这 1 毫秒的延迟足以获取更新的值。
关于python - tkinter SpinBox : delay between updating displayed value and associated StringVar,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46504930/