You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动