返回

解决 CustomTkinter AttributeError: 'Page1' 对象无 'tk' 属性

python

解决 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 类里直接用 CTkButtonCTkLabelCTkImage 这些 CustomTkinter 部件时,这些部件需要找到一个“根”或者“主人” (通常是一个 CTkTk 的实例)。 self.tk 在原生 tkinter 的组件里常见,但在 CustomTkinter, 更常用的是隐式地传递一个父容器 (如一个 CTk 实例)。

在我们这段代码里,问题出在 Page1 类本身没有继承任何 tkinter 或者 CustomTkinter 的类,它只是一个普通的 Python 类。更具体一点,问题出现在__init__ 方法中创建 CTkButtonCTkLabel的时候:

self.lbl1 = CTkLabel(app, text='текст', ...)
self.btn1 = CTkButton(app, text='Начать', ...)

这里你试图将 app (即 CTk 实例) 作为这些部件的父容器。虽然 app 是主窗口,但按照你的设计意图(Page1 类代表一个页面),Page1 类的实例应该有自己的“地盘”(一个 Frame 或者 CTkFrame),然后在这个“地盘”上摆放 LabelButton。直接将 app作为父对象,导致逻辑层级有点混乱。

解决方案

下面提供几种解决方案,一步步教你如何修复这个问题,并改进代码结构。

方案一:让 Page1 继承自 CTkFrame

这是最推荐的做法。既然 Page1 相当于一个页面,那么就让它成为一个 CTkFrameCTkFrame 是 CustomTkinter 里的一个容器部件,可以容纳其他部件。

  1. 原理: 继承 CTkFramePage1 自身成为一个可以放置其他部件的容器,也符合“页面”这个概念的逻辑。
  2. 代码:
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()
  1. 解释:

    • 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, ...):现在,CTkLabelCTkButton 的父容器是 self,也就是 Page1 这个 CTkFrame 实例,不再是 app
    • 我们重写了 showhide方法, 现在仅仅在 show 里使用布局管理器进行展示。
  2. 安全提示: 没什么特别的安全问题需要在这里强调,因为我们处理的主要是 UI 布局。

  3. 进阶

    • 不用每次显示/隐藏页面时都重新设置部件的位置,你可以直接在__init__里面用place()pack()grid()方法设置好. 每次show hide操作时只需要将整个page用布局管理器进行显示/隐藏操作就行.

方案二:使用 tkinter 的 Frame (不推荐,仅供参考)

虽然不推荐,但如果你 非要 使用 tkinter 的 Frame,也是可以的。但请记住,这样你就失去了 CustomTkinter 提供的一些视觉样式和便利功能。

  1. 原理: 与方案一类似,只不过把 CTkFrame 换成了 tkinter 原生的 Frame

  2. 代码:

    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()
    
    
  3. 解释: 除了将CTkFrame 替换为Frame,并且需要手动设置Frame背景颜色, 其它与方案一大同小异.

方案三: 不使用单独的 Page 类 (适合简单界面)

如果你的程序很简单,不需要太多页面,也完全可以将页面内容直接放在主窗口(app)里面,用函数来切换显示和隐藏。

  1. 原理: 直接在主窗口上创建和管理部件,用函数来控制部件的显示和隐藏。
  2. 代码示例
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()

  1. 说明 : 这个适合小项目, 页面元素比较简单, 而且数量较少的情况。

总结与建议

综合来看,方案一 (让 Page1 继承自 CTkFrame) 是最好的选择。它不仅解决了 AttributeError,还让你的代码结构更清晰,更易于维护和扩展。如果你以后需要添加更多的页面,或者在页面中添加更复杂的布局,这种方式会让你事半功倍。 方案二可以作为了解, 方案三适合极其简单的应用.

在GUI编程中,合理地组织代码结构非常重要. 每个类、每个函数都应该有明确的职责, 避免逻辑混乱, 将不同页面的元素分开管理能让你的程序逻辑更清晰。