Tkinter Combobox 绑定事件时初始化报 KeyError
结论
bind() 的第二个参数需要传入可调用对象,不能在这里直接调用函数。可以用 lambda 接收 Tkinter 传入的事件对象,并把 dictionary 和目标 Label 一起传给 calculate():
self.awg_comb.bind(
'<<ComboboxSelected>>',
lambda event: self.calculate(
event, self.awg_dict, self.awg_uitk_label
)
)
self.mm2_comb.bind(
'<<ComboboxSelected>>',
lambda event: self.calculate(
event, self.mm2_dict, self.mm2_uitk_label
)
)
在 calculate() 中通过 event.widget 读取当前 Combobox 的值:
def calculate(self, event, mapping, result_label):
key = event.widget.get()
result_label.configure(text=mapping.get(key, ''))
为什么初始化时会出现 KeyError
下面的代码会在执行 bind() 时立即调用 self.calculate():
self.awg_comb.bind(
'<<ComboboxSelected>>',
self.calculate(self.awg_dict, self.sel_awg.get())
)
这时用户还没有选择任何内容,所以:
self.sel_awg.get()
返回的是空字符串。随后代码访问:
dict_['']
dictionary 中没有这个 key,因此会抛出 KeyError。
同时,self.calculate(...) 的返回值是 None,这段绑定实际上相当于:
self.awg_comb.bind('<<ComboboxSelected>>', None)
即使没有触发 KeyError,事件绑定也无法正常工作。
可直接使用的完整示例
先创建结果 Label,再绑定事件。回调运行时,目标控件就已经存在。
import tkinter as tk
from tkinter import ttk
options = {'padx': 10, 'pady': 5}
uitk_options = {'borderwidth': 5, 'relief': 'sunken'}
class UnitFrame(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.awg_dict = {
'20': '0.50',
'18': '0.75',
'16': '1.50',
'14': '2.50',
'12': '4.00',
'10': '6.00',
'8': '10.00',
'6': '13.30',
'4': '21.10',
'2': '33.60',
'0': '53.50',
'00': '67.40',
'000': '85.00',
'0000': '107.20',
}
self.mm2_dict = {
value: key for key, value in self.awg_dict.items()
}
# AWG to mm2
ttk.Label(self, text='AWG:').grid(
column=0, row=0, **options
)
self.sel_awg = tk.StringVar()
self.awg_comb = ttk.Combobox(
self,
textvariable=self.sel_awg,
values=list(self.awg_dict.keys()),
state='readonly',
)
self.awg_comb.grid(column=1, row=0, **options)
ttk.Label(self, text='MM2:').grid(
column=0, row=1, **options
)
self.awg_uitk_label = ttk.Label(self, **uitk_options)
self.awg_uitk_label.grid(column=1, row=1, **options)
# mm2 to AWG
ttk.Label(self, text='MM2:').grid(
column=2, row=0, **options
)
self.sel_mm2 = tk.StringVar()
self.mm2_comb = ttk.Combobox(
self,
textvariable=self.sel_mm2,
values=list(self.mm2_dict.keys()),
state='readonly',
)
self.mm2_comb.grid(column=3, row=0, **options)
ttk.Label(self, text='AWG:').grid(
column=2, row=1, **options
)
self.mm2_uitk_label = ttk.Label(self, **uitk_options)
self.mm2_uitk_label.grid(column=3, row=1, **options)
self.awg_comb.bind(
'<<ComboboxSelected>>',
lambda event: self.calculate(
event,
self.awg_dict,
self.awg_uitk_label,
),
)
self.mm2_comb.bind(
'<<ComboboxSelected>>',
lambda event: self.calculate(
event,
self.mm2_dict,
self.mm2_uitk_label,
),
)
self.grid(row=0, column=0, sticky='nsew')
def calculate(self, event, mapping, result_label):
key = event.widget.get()
result_label.configure(text=mapping.get(key, ''))
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title('Wiresize calculator')
self.resizable(False, False)
if __name__ == '__main__':
app = App()
unit_frame = UnitFrame(app)
app.mainloop()
回调中的 event 是什么
通过 bind() 注册的回调会收到一个事件对象。对于 <<ComboboxSelected>>,可以通过下面的属性取得触发事件的 Combobox:
event.widget
当前选中的值可以直接这样读取:
key = event.widget.get()
也可以继续读取对应的 StringVar:
self.awg_comb.bind(
'<<ComboboxSelected>>',
lambda event: self.calculate(
self.awg_dict,
self.sel_awg.get(),
self.awg_uitk_label,
),
)
此时函数可以写成:
def calculate(self, mapping, key, result_label):
result_label.configure(text=mapping.get(key, ''))
这两种方式都能正常工作。使用 event.widget.get() 后,回调不必额外接收 StringVar,复用起来更方便。
注意事项
当 key 不存在时,dict.get(key, '') 会返回空字符串,从而避免 KeyError。这里的 Combobox 使用了 state='readonly',用户正常选择的内容都来自 values,所以直接使用 mapping[key] 通常也可以。
下面这种方式生成反向 dictionary 时,原 dictionary 中的 value 必须唯一:
self.mm2_dict = {
value: key for key, value in self.awg_dict.items()
}
如果多个 AWG key 对应同一个 mm2 value,后面的项目会覆盖前面的项目。
备注:内容仅供参考。
