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 模块来解决这个问题,这样可以传递一个参数来停止正在执行的开关的命令功能。这可能是错误的做事方式,但对我有用。
我的 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 模块来解决这个问题,这样可以传递一个参数来停止正在执行的开关的命令功能。这可能是错误的做事方式,但对我有用。