tkinter/customtkinter中实例属性作用域问题:跨Frame开关互斥功能实现报错
问题分析与解决方案
错误原因解析
你碰到的AttributeError完全是代码执行顺序导致的问题:
当你在__init__里创建self.mode_1_switch时,self.mode_2_switch还没有被定义为类的实例属性——Python是从上到下逐行执行__init__方法的,此时self.mode_2_switch根本不存在,lambda表达式里引用它自然会报错。
你疑惑为什么不是NameError?因为你引用的是self.switch_2(实例属性),Python会检查当前实例是否拥有这个属性,找不到就抛出AttributeError;而NameError是当你引用一个未定义的局部/全局变量时才会触发的。
有效解决方案
核心思路是:确保在引用开关对象之前,该对象已经被创建并绑定为实例属性。我们可以分两步实现:先创建所有开关,再为它们绑定互斥逻辑的回调函数,同时用更严谨的deselect()替代toggle()(避免快速点击导致的状态混乱)。
以下是修改后的MainModes类完整代码:
class MainModes(customtkinter.CTkFrame): def __init__(self, parent, controller): customtkinter.CTkFrame.__init__(self, parent) self.controller = controller # overall layout self.grid_columnconfigure(0, weight=1) self.grid_columnconfigure(1, weight=1) self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(1, weight=1) self.grid_rowconfigure(2, weight=1) self.frame = customtkinter.CTkFrame(self) self.frame.grid_rowconfigure(0, weight=1) self.frame.grid_columnconfigure(0, weight=1) # ====================================Mode 1 Frame====================================# self.mode_1_frame = customtkinter.CTkFrame(self.frame) self.mode_1_frame.grid_columnconfigure(0, weight=1) self.mode_1_frame.grid_rowconfigure(0, weight=1) self.mode_1_frame.grid(row=0, column=0, sticky='nsew') # ====================================Mode 2 Frame====================================# self.mode_2_frame = customtkinter.CTkFrame(self.frame) self.mode_2_frame.grid_columnconfigure(0, weight=1) self.mode_2_frame.grid_rowconfigure(0, weight=1) self.mode_2_frame.grid(row=0, column=0, sticky='nsew') # ====================================Mode 1 Frame Widgets====================================# self.mode_1_switch_var = tkinter.StringVar(self.mode_1_frame) self.mode_1_switch_var.set(value='Mode 1: ON') # ====================================Mode_2 Frame Widgets====================================# self.mode_2_switch_var = tkinter.StringVar(self.mode_2_frame) self.mode_2_switch_var.set(value='Mode 2: OFF') # 第一步:先创建两个开关,暂时不绑定command self.mode_1_switch = customtkinter.CTkSwitch( self.mode_1_frame, textvariable=self.mode_1_switch_var, onvalue='ON', offvalue='OFF' ) self.mode_2_switch = customtkinter.CTkSwitch( self.mode_2_frame, textvariable=self.mode_2_switch_var, onvalue='ON', offvalue='OFF' ) # 第二步:现在两个开关都已存在,绑定互斥回调 self.mode_1_switch.configure(command=self._on_mode1_toggled) self.mode_2_switch.configure(command=self._on_mode2_toggled) # 初始化开关状态 self.mode_1_switch.select() self.mode_1_switch.grid(row=0, column=0) self.mode_2_switch.grid(row=0, column=0) # ====================================Frame toggle and back buttons====================================# self.mode_2_button = customtkinter.CTkButton(self, text='Mode 2', command=lambda: self.mode_2_frame.tkraise()) self.mode_1_button = customtkinter.CTkButton(self, text = 'Mode 1', command=lambda: self.mode_1_frame.tkraise()) self.back_button = customtkinter.CTkButton(self, text='Back', command=lambda: controller.show_frame('homescreen')) self.mode_1_button.grid(row=0, column=0, sticky='nsew') self.mode_2_button.grid(row=0, column=1, sticky='nsew') self.frame.grid(row=1, columnspan=2, sticky='nsew') self.back_button.grid(row=2, column=0, columnspan=2, sticky='nsew') self.mode_1_frame.tkraise() # 封装互斥逻辑的回调函数 def _on_mode1_toggled(self): # 更新当前开关文本 self.mode_1_switch_var.set(value=f'Mode 1: {self.mode_1_switch.get()}') # 强制关闭另一个开关并更新文本 if self.mode_1_switch.get() == 'ON': self.mode_2_switch.deselect() self.mode_2_switch_var.set(value='Mode 2: OFF') def _on_mode2_toggled(self): self.mode_2_switch_var.set(value=f'Mode 2: {self.mode_2_switch.get()}') if self.mode_2_switch.get() == 'ON': self.mode_1_switch.deselect() self.mode_1_switch_var.set(value='Mode 1: OFF')
额外优化建议
如果后续需要扩展更多互斥开关,可以把逻辑进一步抽象成通用方法,避免重复代码:
def _handle_mutual_exclusion(self, active_switch, active_var, active_mode, other_switch, other_var, other_mode): active_var.set(value=f'{active_mode}: {active_switch.get()}') if active_switch.get() == 'ON': other_switch.deselect() other_var.set(value=f'{other_mode}: OFF')
然后绑定的时候直接调用:
self.mode_1_switch.configure(command=lambda: self._handle_mutual_exclusion( self.mode_1_switch, self.mode_1_switch_var, 'Mode 1', self.mode_2_switch, self.mode_2_switch_var, 'Mode 2' ))
内容的提问来源于stack exchange,提问作者Matthew Axe




