解决 CustomTkinter AttributeError: 'Page1' 对象无 'tk' 属性
2025-03-08 01:46:46
解决 CustomTkinter 中的 AttributeError: 'Page1' object has no attribute 'tk' 错误
碰到了 AttributeError: 'Page1' object has no attribute 'tk'
这个错误? 别急,我们来一步步解决它。这通常是因为在 CustomTkinter 中创建自定义部件(widget)时,没有正确地初始化父类,或者混淆了 tkinter 和 CustomTkinter 的部件。
问题根源
错误信息 'Page1' object has no attribute 'tk'
意思是说,Page1
这个类的实例对象没有一个名为 tk
的属性。 CustomTkinter 是基于 tkinter 的,它的部件实际上是 tkinter 部件的扩展或者包装。 当你在 Page1
类里直接用 CTkButton
、CTkLabel
、CTkImage
这些 CustomTkinter 部件时,这些部件需要找到一个“根”或者“主人” (通常是一个 CTk
或 Tk
的实例)。 self.tk
在原生 tkinter 的组件里常见,但在 CustomTkinter, 更常用的是隐式地传递一个父容器 (如一个 CTk
实例)。
在我们这段代码里,问题出在 Page1
类本身没有继承任何 tkinter 或者 CustomTkinter 的类,它只是一个普通的 Python 类。更具体一点,问题出现在__init__
方法中创建 CTkButton
和 CTkLabel
的时候:
self.lbl1 = CTkLabel(app, text='текст', ...)
self.btn1 = CTkButton(app, text='Начать', ...)
这里你试图将 app
(即 CTk
实例) 作为这些部件的父容器。虽然 app
是主窗口,但按照你的设计意图(Page1
类代表一个页面),Page1
类的实例应该有自己的“地盘”(一个 Frame 或者 CTkFrame),然后在这个“地盘”上摆放 Label
和 Button
。直接将 app
作为父对象,导致逻辑层级有点混乱。
解决方案
下面提供几种解决方案,一步步教你如何修复这个问题,并改进代码结构。
方案一:让 Page1
继承自 CTkFrame
这是最推荐的做法。既然 Page1
相当于一个页面,那么就让它成为一个 CTkFrame
。CTkFrame
是 CustomTkinter 里的一个容器部件,可以容纳其他部件。
- 原理: 继承
CTkFrame
让Page1
自身成为一个可以放置其他部件的容器,也符合“页面”这个概念的逻辑。 - 代码:
from tkinter import *
from customtkinter import *
from PIL import Image
app = CTk()
app.configure(fg_color='#dda15e')
app.title('School Project')
app.resizable(width=False, height=False)
app.geometry('690x520')
def hide_all():
""" 隐藏所有页面 """
page1.pack_forget() # 或者用 page1.grid_forget(),取决于你用的布局管理器
def show_page1():
page1.pack() # 或者用 page1.grid(),取决于你用的布局管理器
#注意:这里不能再用page1.show(),因为我们重写了show方法,这里我们只需要调用布局管理器来展示页面
class Page1(CTkFrame): # 让 Page1 继承自 CTkFrame
def __init__(self, master): # master形参,接收父容器
super().__init__(master) # 调用父类 CTkFrame 的初始化方法, 传入master
self.configure(fg_color='#dda15e') #设置Page1的背景颜色
self.on_img = CTkImage(light_image=Image.open("main1.png"), size=(300, 400))
self.il = CTkButton(self, image=self.on_img, text="") # 这里的 self 就是 Page1 这个 CTkFrame
self.lbl1 = CTkLabel(self, text='текст', font=('Calibri', 25), width=10, height=10, text_color='#fefae0')
self.btn1 = CTkButton(self, text='Начать', font=('Arial', 20), command=show_page1, text_color='#fefae0',
width=150,
height=50, fg_color='#bc6c25', border_color='#fefae0', border_width=2,
hover_color='#d4a373')
# 注意:现在 lbl1 和 btn1 的父容器都是 self (即 Page1),而不是 app
self.lbl1.place(x=40, y=230)
self.btn1.place(x=260, y=380)
#可以考虑在这里使用pack() 或者 grid() 进行布局
def show(self): # 重写show()方法
hide_all()
self.pack()#用布局管理器来显示,也可用grid().
#布局移动到了__init__里
def hide(self):
self.pack_forget() # 或者用self.grid_forget()
#布局移动到了__init__里
page1 = Page1(app) # 创建 Page1 实例, app作为父容器,会作为master形参传入到__init__()
page1.pack() #先用布局管理器pack起来,
app.mainloop()
-
解释:
class Page1(CTkFrame):
让Page1
继承CTkFrame
。__init__(self, master):
现在__init__
接受一个master
参数,代表这个Page1
实例的父容器。super().__init__(master)
调用父类CTkFrame
的初始化方法,并把master
传递给它。这样Page1
才能正确地作为一个CTkFrame
工作。self.lbl1 = CTkLabel(self, ...)
和self.btn1 = CTkButton(self, ...)
:现在,CTkLabel
和CTkButton
的父容器是self
,也就是Page1
这个CTkFrame
实例,不再是app
。- 我们重写了
show
和hide
方法, 现在仅仅在show
里使用布局管理器进行展示。
-
安全提示: 没什么特别的安全问题需要在这里强调,因为我们处理的主要是 UI 布局。
-
进阶
- 不用每次显示/隐藏页面时都重新设置部件的位置,你可以直接在
__init__
里面用place()
、pack()
、grid()
方法设置好. 每次show
hide
操作时只需要将整个page用布局管理器进行显示/隐藏操作就行.
- 不用每次显示/隐藏页面时都重新设置部件的位置,你可以直接在
方案二:使用 tkinter 的 Frame
(不推荐,仅供参考)
虽然不推荐,但如果你 非要 使用 tkinter 的 Frame
,也是可以的。但请记住,这样你就失去了 CustomTkinter 提供的一些视觉样式和便利功能。
-
原理: 与方案一类似,只不过把
CTkFrame
换成了 tkinter 原生的Frame
。 -
代码:
from tkinter import * from customtkinter import * from PIL import Image app = CTk() app.configure(fg_color='#dda15e') app.title('School Project') app.resizable(width=False, height=False) app.geometry('690x520') def hide_all(): """ 隐藏所有页面 """ page1.pack_forget() def show_page1(): page1.pack() class Page1(Frame): # 继承自 tkinter 的 Frame def __init__(self, master): super().__init__(master) # master传进去 self.configure(bg='#dda15e') # 针对tkinter Frame的背景颜色设置 #CustomTkinter的图片和按钮 self.on_img = CTkImage(light_image=Image.open("main1.png"), size=(300, 400)) self.il = CTkButton(self, image=self.on_img, text="") #其余部件仍然可以使用CustomTkinter的 self.lbl1 = CTkLabel(self, text='текст', font=('Calibri', 25), width=10, height=10, text_color='#fefae0') self.btn1 = CTkButton(self, text='Начать', font=('Arial', 20), command=show_page1, text_color='#fefae0', width=150, height=50, fg_color='#bc6c25', border_color='#fefae0', border_width=2, hover_color='#d4a373') self.lbl1.place(x=40, y=230) self.btn1.place(x=260, y=380) def show(self): hide_all() self.pack() def hide(self): self.pack_forget() page1 = Page1(app) # app作为 master 参数传给Page1 page1.pack() app.mainloop()
-
解释: 除了将
CTkFrame
替换为Frame
,并且需要手动设置Frame背景颜色, 其它与方案一大同小异.
方案三: 不使用单独的 Page 类 (适合简单界面)
如果你的程序很简单,不需要太多页面,也完全可以将页面内容直接放在主窗口(app
)里面,用函数来切换显示和隐藏。
- 原理: 直接在主窗口上创建和管理部件,用函数来控制部件的显示和隐藏。
- 代码示例
from tkinter import *
from customtkinter import *
from PIL import Image
app = CTk()
app.configure(fg_color='#dda15e')
app.title('School Project')
app.resizable(width=False, height=False)
app.geometry('690x520')
def show_page1_widgets():
lbl1.pack() #用你喜欢的方式显示, 这里仅作示例
btn1.pack()
def hide_page1_widgets():
lbl1.pack_forget()
btn1.pack_forget()
# 直接在 app 上创建部件
on_img = CTkImage(light_image=Image.open("main1.png"), size=(300, 400))
il = CTkButton(app, image=on_img, text="") #注意,现在父容器直接是 app
lbl1 = CTkLabel(app, text='текст', font=('Calibri', 25), width=10, height=10, text_color='#fefae0')
btn1 = CTkButton(app, text='Начать', font=('Arial', 20), command=show_page1_widgets, text_color='#fefae0', width=150,
height=50, fg_color='#bc6c25', border_color='#fefae0', border_width=2, hover_color='#d4a373')
#一开始就显示
show_page1_widgets()
app.mainloop()
- 说明 : 这个适合小项目, 页面元素比较简单, 而且数量较少的情况。
总结与建议
综合来看,方案一 (让 Page1
继承自 CTkFrame
) 是最好的选择。它不仅解决了 AttributeError
,还让你的代码结构更清晰,更易于维护和扩展。如果你以后需要添加更多的页面,或者在页面中添加更复杂的布局,这种方式会让你事半功倍。 方案二可以作为了解, 方案三适合极其简单的应用.
在GUI编程中,合理地组织代码结构非常重要. 每个类、每个函数都应该有明确的职责, 避免逻辑混乱, 将不同页面的元素分开管理能让你的程序逻辑更清晰。