使用tkinter/customtkinter切换页面时出现after脚本无效命令名错误
问题
用tkinter和customtkinter开发带登录页的应用,输入有效凭据后跳转至账户页,功能正常但持续报错invalid command name while executing ("after" script),报错信息如下:
invalid command name "2792697387712check_dpi_scaling" while executing "2792697387712check_dpi_scaling" ("after" script) invalid command name "2792697396224update" while executing "2792697396224update" ("after" script) invalid command name "2792696741952_click_animation" while executing "2792696741952_click_animation" ("after" script)
登录页代码:
class LoginPage: def __init__(self) -> None: self.root = ctk.CTk() ctk.set_appearance_mode("dark") ctk.set_default_color_theme("dark-blue") self.root.after(0, lambda:self.root.state('zoomed')) self.root.title("Login Page") bgImage = Image.open("bg.jpg") bg = ImageTk.PhotoImage(bgImage) bgLabel = tk.Label(self.root, image = bg) bgLabel.place(x = 0, y = 0, relwidth = 1, relheight = 1) loginFrame = ctk.CTkFrame(self.root) loginFrame.pack(pady = 120, padx = 500, fill = "both", expand = True) loginLabel = ctk.CTkLabel(loginFrame, text="Login", font = ("Tahoma", 35, "bold")) loginLabel.pack(pady = 30, padx = 10) self.usernameEntry = ctk.CTkEntry(loginFrame, placeholder_text="Username", font = ("Tahoma", 15), width = 300, height = 50) self.usernameEntry.pack(pady = 30, padx = 10) self.passwordEntry = ctk.CTkEntry(loginFrame, placeholder_text="Password", show = "*", font = ("Tahoma", 15), width = 300, height = 50) self.passwordEntry.pack(pady = 30, padx = 10) loginButton = ctk.CTkButton(loginFrame, text="Login", font = ("Tahoma", 15, "bold"), command = self.validateLogin, width = 300, height = 50) loginButton.pack(pady = 30, padx = 10) self.root.mainloop() def validateLogin(self) -> None: name = self.usernameEntry.get().lower() if self.checkProvidedLogin(name, self.passwordEntry.get()): self.root.destroy() AccountPage(name) else: messagebox.showerror(message = "Invalid login, Try again") def checkProvidedLogin(self, username, password) -> bool: with open('logins.csv', 'r') as file: logins = file.read().splitlines() for line in logins: if line == username + "," + password: return True return False if __name__ == "__main__": LoginPage()
账户页代码:
class AccountPage: def __init__(self, username) -> None: self.accountRoot = ctk.CTk() ctk.set_appearance_mode("dark") ctk.set_default_color_theme("dark-blue") self.accountRoot.after(0, lambda:self.accountRoot.state('zoomed')) bgImage = Image.open("bg.jpg") bg = ImageTk.PhotoImage(bgImage) bgLabel = tk.Label(self.accountRoot, image = bg) bgLabel.place(x = 0, y = 0, relwidth = 1, relheight = 1) accountFrame = ctk.CTkFrame(self.accountRoot) accountFrame.pack(pady = 50, padx = 50, fill = "both", expand = True) welcomeLabel = ctk.CTkLabel(accountFrame, text = f"Welcome, {username}!") welcomeLabel.pack(pady = 20) self.accountRoot.mainloop()
尝试调换self.root.destroy()和AccountPage(name)的顺序后问题仍存在,请问错误原因是什么?
错误原因及解决方案
核心原因
CustomTkinter的组件(比如CTkButton、CTkEntry)会在后台创建**after定时任务**,比如按钮点击动画、DPI缩放检查、组件状态更新等,这些任务会绑定到登录页的root窗口上。
当你调用self.root.destroy()销毁登录窗口时,这些已经被加入主循环队列的after任务并没有被取消。tkinter主循环仍会尝试执行这些任务,但此时任务绑定的窗口和组件已经被销毁回收,就会抛出invalid command name错误。
哪怕调换destroy和实例化AccountPage的顺序也没用——因为AccountPage的mainloop会接管,但登录页的after任务已经在队列里了,销毁窗口后任务依然存在,执行时必然报错。
解决方式
临时修复:延迟销毁窗口
给销毁操作加个短延迟,让组件内部的after任务有时间完成,再销毁窗口:
def validateLogin(self) -> None: name = self.usernameEntry.get().lower() if self.checkProvidedLogin(name, self.passwordEntry.get()): # 延迟100ms销毁,给pending的after任务留完成时间 self.root.after(100, lambda: (self.root.destroy(), AccountPage(name))) else: messagebox.showerror(message = "Invalid login, Try again")
推荐方案:改用单窗口多Frame架构
从根本上避免窗口销毁带来的问题,所有页面都作为主窗口的Frame,切换时只需要隐藏当前Frame、显示目标Frame:
import tkinter as tk import customtkinter as ctk from PIL import Image, ImageTk from tkinter import messagebox class App: def __init__(self): self.root = ctk.CTk() ctk.set_appearance_mode("dark") ctk.set_default_color_theme("dark-blue") self.root.state('zoomed') self.root.title("App") # 创建两个页面Frame self.login_frame = LoginPage(self.root, self.show_account_page) self.account_frame = AccountPage(self.root) # 初始显示登录页 self.login_frame.pack(fill="both", expand=True) self.root.mainloop() def show_account_page(self, username): # 隐藏登录页,显示账户页并更新用户名 self.login_frame.pack_forget() self.account_frame.update_welcome(username) self.account_frame.pack(fill="both", expand=True) class LoginPage(ctk.CTkFrame): def __init__(self, parent, switch_callback): super().__init__(parent) self.switch_callback = switch_callback bgImage = Image.open("bg.jpg") bg = ImageTk.PhotoImage(bgImage) bgLabel = tk.Label(self, image = bg) bgLabel.place(x = 0, y = 0, relwidth = 1, relheight = 1) bgLabel.image = bg # 保存引用防止图片被GC回收 loginFrame = ctk.CTkFrame(self) loginFrame.pack(pady = 120, padx = 500, fill = "both", expand = True) loginLabel = ctk.CTkLabel(loginFrame, text="Login", font = ("Tahoma", 35, "bold")) loginLabel.pack(pady = 30, padx = 10) self.usernameEntry = ctk.CTkEntry(loginFrame, placeholder_text="Username", font = ("Tahoma", 15), width = 300, height = 50) self.usernameEntry.pack(pady = 30, padx = 10) self.passwordEntry = ctk.CTkEntry(loginFrame, placeholder_text="Password", show = "*", font = ("Tahoma", 15), width = 300, height = 50) self.passwordEntry.pack(pady = 30, padx = 10) loginButton = ctk.CTkButton(loginFrame, text="Login", font = ("Tahoma", 15, "bold"), command = self.validateLogin, width = 300, height = 50) loginButton.pack(pady = 30, padx = 10) def validateLogin(self) -> None: name = self.usernameEntry.get().lower() if self.checkProvidedLogin(name, self.passwordEntry.get()): self.switch_callback(name) else: messagebox.showerror(message = "Invalid login, Try again") def checkProvidedLogin(self, username, password) -> bool: with open('logins.csv', 'r') as file: logins = file.read().splitlines() for line in logins: if line == username + "," + password: return True return False class AccountPage(ctk.CTkFrame): def __init__(self, parent): super().__init__(parent) bgImage = Image.open("bg.jpg") bg = ImageTk.PhotoImage(bgImage) bgLabel = tk.Label(self, image = bg) bgLabel.place(x = 0, y = 0, relwidth = 1, relheight = 1) bgLabel.image = bg # 保存引用防止图片被GC回收 accountFrame = ctk.CTkFrame(self) accountFrame.pack(pady = 50, padx = 50, fill = "both", expand = True) self.welcomeLabel = ctk.CTkLabel(accountFrame, text = "") self.welcomeLabel.pack(pady = 20) def update_welcome(self, username): self.welcomeLabel.configure(text=f"Welcome, {username}!") if __name__ == "__main__": App()
这种架构不仅解决了after任务的问题,页面切换也更流畅,是tkinter开发的标准最佳实践。
内容的提问来源于stack exchange,提问作者JPthunder

