Home > front end >  Scoping issue with instance attributes and Class methods in tkinter/customtkinter
Scoping issue with instance attributes and Class methods in tkinter/customtkinter

Time:05-14

My tkinter/customtkinter GUI uses multiple frames raised above one another. I have a two frames both containing switches that need to invert one another (ie when one is 'ON' the other must be 'OFF'). Both switches are created as instance objects within the init method use a function within their 'command' parameter to alter one another's state.

Initially I tried to directly call each object:

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())

This produces the following error:

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

I assumed this was as a switch_2 was referenced before it was defined but I'm not confident in my understanding as I thought if that was the case it would produce a NameError (I guess this is the same error dynamic but since I am in a class it is an AttributeError?).

I instead tried to create a method to handle this:

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()

This produces the following error:

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

With the only difference here being that the wording has changed from 'object' to attribute'.

Finally I tried to handle it with a funcion within the init method but predictably this failed:

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))

    

This produces the original error:

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

I know it is a scoping issue as if I remove the function from the switch_1 object command parameter then switch_2 functions as desired. I am sure this is a duplicate issue, I have looked amongst the problems but I couldn't find a solution.

Additionally I find this difficult to understand as within the same code I have buttons that reference functions created within the init method that reconfigure one another and I don't experience any of these errors. I have thoroughly confused myself.

edit: I thought the original examples might give enough information to conceptually see what is going on, however I understand they don't replicate the problem. I have included below a small fully working example that shows the basic structure of the GUI (probably gone too far the other way now):

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()

CodePudding user response:

'Try configuring the switches' commands after creating them:

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

and

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

at the end of the init() method. – Sylvester Kruin'

This was definitely the correct approach, unfortunately the issue is (that I really should have seen!) that whenever either switch is toggled it creates an infinite exchange of callback functions as they keep toggling each other back and forth. I got around this by putting a control parameter into the customtkinter module so an argument can be passed that stops the command function of the switch being executed. This is probably the wrong way to do things but it worked for me.

  • Related