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.