tkinter/customtkinter 中实例属性和 Class 方法的范围问题

Scoping issue with instance attributes and Class methods in tkinter/customtkinter

我的 tkinter/customtkinter GUI 使用了多个框架,一个框架在另一个框架之上。我有两个框架都包含需要相互反转的开关(即,当一个是 'ON' 时,另一个必须是 'OFF')。这两个开关都是在 init 方法中作为实例对象创建的,使用它们的 'command' 参数中的一个函数来改变彼此的状态。

最初我尝试直接调用每个对象:

class MainModes(customtkinter.CTkFrame):
    def__init__(self, parent)
        customtkinter.CTkframe.__init__(self, parent)
        
        self.frame_1 = customtkinter.CTkFrame(self)
        self.frame_1.pack()

        self.frame_2 = customtkinter.CTkFrame(self)
        self.frame_2.pack()

        self.switch_1 = customtkinter.CTkSwitch(self.frame_1,
                                                text='Switch 1',
                                                command=lambda: self.switch_2.toggle())

        self.switch_2 = customtkinter.CTkSwitch(self.frame_2,
                                                text='Switch 2',
                                                command=lambda: self.switch_1.toggle())

这会产生以下错误:

    command=lambda: self.switch_2.toggle() 
AttributeError: 'MainModes' has no object 'switch_2'

我认为这是因为 switch_2 在定义之前被引用了,但我对自己的理解没有信心,因为我认为如果是这种情况会产生 NameError(我猜这是同样的错误动态,但因为我在 class 中,所以它是一个 AttributeError?)。

我改为尝试创建一种方法来处理此问题:

class MainModes(customtkinter.CTkFrame):
    def__init__(self, parent)
        customtkinter.CTkframe.__init__(self, parent)
         
        self.frame_1 = customtkinter.CTkFrame(self)
        self.frame_1.pack()

        self.frame_2 = customtkinter.CTkFrame(self)
        self.frame_2.pack()

        self.switch_1 = customtkinter.CTkSwitch(self.frame_1,
                                                text='Switch 1',
                                                command=lambda: self.toggle_switch(switch_2))

        self.switch_2 = customtkinter.CTkSwitch(self.frame_2,
                                                text='Switch 2',
                                                command=lambda: self.toggle_switch(switch_1))

    def toggle_switch(self, switch):
          self.switch.toggle()

这会产生以下错误:

    command=lambda: self.toggle_switch(self.switch_2) 
AttributeError: 'MainModes' has no attribute 'switch_2'

这里唯一的区别是措辞已从 'object' 更改为属性'。

最后我尝试用 init 方法中的函数来处理它,但不出所料这失败了:

class MainModes(customtkinter.CTkFrame):
    def__init__(self, parent)
        customtkinter.CTkframe.__init__(self, parent)
         
        self.frame_1 = customtkinter.CTkFrame(self)
        self.frame_1.pack()

        self.frame_2 = customtkinter.CTkFrame(self)
        self.frame_2.pack()

        def toggle_switch(switch):
            self.switch.toggle()

        self.switch_1 = customtkinter.CTkSwitch(self.frame_1,
                                                text='Switch 1',
                                                command=lambda: toggle_switch(switch_2))

        self.switch_2 = customtkinter.CTkSwitch(self.frame_2,
                                                text='Switch 2',
                                                command=lambda: toggle_switch(switch_1))

    

这会产生原始错误:

    command=lambda: self.switch_2.toggle() 
AttributeError: 'MainModes' has no object 'switch_2'

我知道这是一个范围问题,就好像我从 switch_1 对象命令参数中删除了该函数,然后 switch_2 就可以按需运行了。我确定这是一个重复的问题,我已经查看了问题,但找不到解决方案。

此外,我发现这很难理解,因为在同一代码中,我有一些按钮引用了在 init 方法中创建的函数,这些函数相互重新配置,而且我没有遇到任何这些错误。我把自己彻底搞糊涂了。

编辑:我认为原始示例可能会提供足够的信息以从概念上了解发生了什么,但我知道它们不会重现问题。我在下面包含了一个完整的小示例,它显示了 GUI 的基本结构(现在可能走得太远了):

import tkinter
import customtkinter

# Main application    
class App(customtkinter.CTk):

    def __init__(self):
        super().__init__()
                    
        #container to pack different windows of the app into
        container = customtkinter.CTkFrame(self)
        container.pack(expand=True, fill='both')
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)
    
        self.frames = {}
        self.frames['homescreen'] = HomeScreen(container, self)
        self.frames['page_1'] = MainModes(container, self)
           
        for F in ('homescreen', 'page_1'):
        
            self.frames[F].grid(row = 0, column = 0, sticky='nsew')
    
        self.show_frame('homescreen')
    
    def show_frame(self, page_class):
        frame = self.frames[page_class]
        frame.tkraise()
     
class HomeScreen(customtkinter.CTkFrame):
    def __init__(self, parent, controller):
        customtkinter.CTkFrame.__init__(self, parent)
            
        self.controller = controller
            
        #Configure rows and columns
        self.grid_rowconfigure(0, weight=1) 
        self.grid_rowconfigure(1, weight=1)
                    
        #Define buttons
        page_1_button = customtkinter.CTkButton(self,                                                 
                                                text="Page 1",
                                                command=lambda: controller.show_frame('page_1'))

        #Position of buttons in the main_window
        page_1_button.grid(row=0, column=0, sticky='nsew')
    
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) #mode_1 and mode_2 tabs are contained here
        self.grid_rowconfigure(1, weight=1) #all widgets are contained in two frames in this row, clicking between mode_1 and mode_2 buttons raises different frames containing different widgets
        self.grid_rowconfigure(2, weight=1) #back button is here
           
        self.frame = customtkinter.CTkFrame(self) #this frame contains the mode_1 and mode_2 frames and they are raised over one another according to which tab is selected
        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')
    
        #function that sets the textvariable values of mode_1_switch and mode_2_switch when either is toggled
        def switch_functions(switch_var, mode, switch):
            switch_var.set(value=f'{mode}: ' + switch.get())
                                              
        self.mode_1_switch = customtkinter.CTkSwitch(self.mode_1_frame,
                                                 textvariable=self.mode_1_switch_var,
                                                 onvalue='ON',
                                                 offvalue='OFF',
                                                 command=lambda: [switch_functions(self.mode_1_switch_var, 'Mode 1', self.mode_1_switch), self.mode_2_switch.toggle()])
    
        self.mode_1_switch.select()#turns switch on at open
        self.mode_1_switch.grid(row=0, column=0)          
         
        #====================================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')
    
                
        self.mode_2_switch = customtkinter.CTkSwitch(self.mode_2_frame,
                                                 textvariable=self.mode_2_switch_var,
                                                 onvalue='ON',
                                                 offvalue='OFF',
                                                 command=lambda: [switch_functions(self.mode_2_switch_var, 'Mode 2', self.mode_2_switch), self.mode_1_switch.toggle()])
     
        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()

if __name__ == '__main__':
    app = App()
    app.mainloop()

'Try configuring the switches' 创建后的命令:

self.switch_1.config(command=lambda: self.switch_2.toggle())

self.switch_2.config(command=lambda: self.switch_1.toggle()) 

init() 方法的末尾。 – 西尔维斯特克鲁因

这绝对是正确的方法,不幸的是,问题是(我真的应该看到!)每当切换任一开关时,它都会创建无限的回调函数交换,因为它们不断相互来回切换。我通过将控制参数放入 customtkinter 模块来解决这个问题,这样可以传递一个参数来停止正在执行的开关的命令功能。这可能是错误的做事方式,但对我有用。