精通 CustomTkinter:轻松构建精美的 Python GUI
先把话说在前面哈,CustomTkinter 这个东西,如果你还在用原生 Tkinter 写界面,你大概率会有那种:功能是能用的,就是丑得有点对不起自己审美…的感觉。
我也是之前帮同事做个小工具,原生 Tkinter 拉了个界面出来,他一句话把我说回去了:“这玩意儿看着像 2005 年的系统弹窗”。然后我就去认真用了一圈 CustomTkinter,发现还真挺香,界面质感直接上了一个档次,而且用法对 Tkinter 用户特别友好,几乎是“平滑迁移”。
下面就边聊天边写代码,带你把 CustomTkinter 从入门到能写出一个还算精致的小 GUI。
先搞定环境和第一个窗口
假设你本地已经有 Python 3.8+。装一下库:
pip install customtkinter
然后来个最小可运行的小例子,先感受一下“颜值差距”:
import customtkinter as ctk# 1. 全局外观设置
ctk.set_appearance_mode("dark") # "light" / "dark" / "system"
ctk.set_default_color_theme("blue") # 内置: "blue", "green", "dark-blue"
# 2. 创建主窗口(注意是 CTk,不是 Tk)
app = ctk.CTk()
app.title("CustomTkinter Demo")
app.geometry("400x300")
# 3. 放一个 Label
label = ctk.CTkLabel(app, text="Hello, CustomTkinter 👋", font=("微软雅黑", 18))
label.pack(pady=30)
# 4. 一个按钮,点一下改文字
defon_click():
label.configure(text="按钮被点了~")
button = ctk.CTkButton(app, text="点我试试", command=on_click)
button.pack(pady=10)
# 5. 事件循环
app.mainloop()
你跑一下,基本能立刻感受到:字体、圆角、阴影、颜色,这些“细节活”都帮你做好了,你专心撸业务逻辑就行。
几个核心概念先拎一拎
跟原生 Tkinter 比起来,CustomTkinter 的基本思想其实没变:
原来的 Tk换成了CTk原来的 Button / Label / Entry等,换成CTkButton / CTkLabel / CTkEntry原来的布局(pack / grid / place)照用
最常用的几个组件你基本就这几类:
CTkLabel:文字提示CTkEntry:输入框CTkButton:按钮CTkFrame:容器(做布局的)CTkTabview:多标签页CTkTextbox:多行文本CTkComboBox/CTkOptionMenu:下拉选择
所以你可以这么理解:如果你会 Tkinter,CustomTkinter 就是换个前端 UI 库,不用换“思想”。
做一个稍微像样点的登录窗口
随便写个“登录窗”这种小工具是最典型的使用场景,我们直接上代码:
import customtkinter as ctkctk.set_appearance_mode("light")
ctk.set_default_color_theme("blue")
classLoginApp(ctk.CTk):
def__init__(self):
super().__init__()
self.title("登录系统")
self.geometry("420x260")
self.resizable(False, False)
# 外层居中容器
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
main_frame = ctk.CTkFrame(self)
main_frame.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
# 内部网格布局
for i in range(4):
main_frame.grid_rowconfigure(i, weight=0)
main_frame.grid_columnconfigure(0, weight=0)
main_frame.grid_columnconfigure(1, weight=1)
title = ctk.CTkLabel(main_frame, text="欢迎登录 👇", font=("微软雅黑", 20, "bold"))
title.grid(row=0, column=0, columnspan=2, pady=(10, 20))
# 用户名
user_label = ctk.CTkLabel(main_frame, text="用户名:")
user_label.grid(row=1, column=0, padx=(10, 5), pady=5, sticky="e")
self.user_entry = ctk.CTkEntry(main_frame, width=220, placeholder_text="请输入用户名")
self.user_entry.grid(row=1, column=1, padx=(5, 10), pady=5, sticky="w")
# 密码
pwd_label = ctk.CTkLabel(main_frame, text="密码:")
pwd_label.grid(row=2, column=0, padx=(10, 5), pady=5, sticky="e")
self.pwd_entry = ctk.CTkEntry(main_frame, width=220, show="*", placeholder_text="请输入密码")
self.pwd_entry.grid(row=2, column=1, padx=(5, 10), pady=5, sticky="w")
# 记住我 + 主题切换
self.remember_var = ctk.BooleanVar(value=True)
remember_cb = ctk.CTkCheckBox(main_frame, text="记住我", variable=self.remember_var)
remember_cb.grid(row=3, column=0, padx=10, pady=10, sticky="w")
self.theme_switch = ctk.CTkSwitch(
main_frame,
text="暗色模式",
command=self.toggle_theme
)
self.theme_switch.grid(row=3, column=1, padx=10, pady=10, sticky="e")
# 登录按钮 + 状态栏
login_btn = ctk.CTkButton(main_frame, text="登录", width=100, command=self.handle_login)
login_btn.grid(row=4, column=0, columnspan=2, pady=(10, 5))
self.status_label = ctk.CTkLabel(main_frame, text="", text_color="gray")
self.status_label.grid(row=5, column=0, columnspan=2, pady=(0, 8))
# 回车触发登录
self.bind("<Return>", lambda event: self.handle_login())
deftoggle_theme(self):
current = ctk.get_appearance_mode() # "Light" / "Dark"
if current == "Light":
ctk.set_appearance_mode("dark")
self.theme_switch.configure(text="暗色模式")
else:
ctk.set_appearance_mode("light")
self.theme_switch.configure(text="亮色模式")
defhandle_login(self):
username = self.user_entry.get().strip()
password = self.pwd_entry.get().strip()
ifnot username ornot password:
self.status_label.configure(text="用户名或密码不能为空", text_color="red")
return
# 这里可以换成真正的校验逻辑,比如访问接口
if username == "admin"and password == "123456":
remember = self.remember_var.get()
self.status_label.configure(
text=f"登录成功,记住我:{remember}",
text_color="green"
)
else:
self.status_label.configure(text="账号或密码错误", text_color="red")
if __name__ == "__main__":
app = LoginApp()
app.mainloop()
这段代码里,你顺便学掉了好几个关键点:
全局外观模式 & 主题: set_appearance_mode/set_default_color_themeFrame + grid 做布局,让控件看起来“不挤” CTkEntry的placeholder_text、show="*"这种小细节CTkCheckBox+BooleanVar做状态绑定CTkSwitch直接实现暗黑模式切换键盘事件 <Return>绑定登录
写业务的时候,基本就围着这几个组件打转,再配合一点请求逻辑,就可以跑去连后端了。
多页界面:用 Tabview 写一个小工具面板
很多桌面小工具都喜欢用“多标签页”来分功能,比如“配置”“日志”“关于”等。CustomTkinter 里直接用 CTkTabview 来搞:
import customtkinter as ctkctk.set_appearance_mode("system")
ctk.set_default_color_theme("green")
classToolApp(ctk.CTk):
def__init__(self):
super().__init__()
self.title("开发小工具箱")
self.geometry("640x420")
# 顶部标题
title = ctk.CTkLabel(self, text="Python GUI 工具箱", font=("微软雅黑", 22, "bold"))
title.pack(pady=(15, 5))
# Tab 容器
tabview = ctk.CTkTabview(self, width=600, height=330)
tabview.pack(padx=10, pady=10, fill="both", expand=True)
# 创建三个 tab
tab_config = tabview.add("配置生成")
tab_log = tabview.add("日志查看")
tab_about = tabview.add("关于")
self._build_config_tab(tab_config)
self._build_log_tab(tab_log)
self._build_about_tab(tab_about)
def_build_config_tab(self, parent):
parent.grid_columnconfigure(0, weight=0)
parent.grid_columnconfigure(1, weight=1)
name_label = ctk.CTkLabel(parent, text="服务名:")
name_label.grid(row=0, column=0, padx=10, pady=10, sticky="e")
self.name_entry = ctk.CTkEntry(parent, placeholder_text="例如: user-service")
self.name_entry.grid(row=0, column=1, padx=10, pady=10, sticky="we")
port_label = ctk.CTkLabel(parent, text="端口号:")
port_label.grid(row=1, column=0, padx=10, pady=10, sticky="e")
self.port_entry = ctk.CTkEntry(parent, placeholder_text="例如: 8080")
self.port_entry.grid(row=1, column=1, padx=10, pady=10, sticky="we")
env_label = ctk.CTkLabel(parent, text="环境:")
env_label.grid(row=2, column=0, padx=10, pady=10, sticky="e")
self.env_combo = ctk.CTkComboBox(parent, values=["dev", "test", "prod"])
self.env_combo.set("dev")
self.env_combo.grid(row=2, column=1, padx=10, pady=10, sticky="w")
gen_btn = ctk.CTkButton(parent, text="生成配置", command=self.generate_config)
gen_btn.grid(row=3, column=0, columnspan=2, pady=10)
self.config_text = ctk.CTkTextbox(parent, height=120)
self.config_text.grid(row=4, column=0, columnspan=2, padx=10, pady=10, sticky="nsew")
parent.grid_rowconfigure(4, weight=1)
def_build_log_tab(self, parent):
parent.grid_rowconfigure(0, weight=1)
parent.grid_columnconfigure(0, weight=1)
self.log_text = ctk.CTkTextbox(parent)
self.log_text.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
load_btn = ctk.CTkButton(parent, text="模拟加载日志", command=self.load_logs)
load_btn.grid(row=1, column=0, padx=10, pady=5, sticky="e")
def_build_about_tab(self, parent):
parent.grid_columnconfigure(0, weight=1)
info = (
"一个用 CustomTkinter 写的示例工具箱\n"
"用于演示 Tabview + Textbox + ComboBox 的基础用法\n\n"
"你可以在这个框架上随便加自己的功能 🙂"
)
label = ctk.CTkLabel(parent, text=info, justify="left")
label.grid(row=0, column=0, padx=20, pady=20, sticky="nw")
defgenerate_config(self):
name = self.name_entry.get().strip()
port = self.port_entry.get().strip()
env = self.env_combo.get()
ifnot name ornot port:
self.config_text.delete("1.0", "end")
self.config_text.insert("1.0", "# 请先填写服务名和端口号\n")
return
config = f"""# 自动生成的配置
app.name={name}
app.port={port}
app.env={env}
"""
self.config_text.delete("1.0", "end")
self.config_text.insert("1.0", config)
defload_logs(self):
demo = "[INFO] 应用启动成功\n[INFO] 正在监听端口 8080\n[WARN] 暂无更多日志..."
self.log_text.delete("1.0", "end")
self.log_text.insert("1.0", demo)
if __name__ == "__main__":
app = ToolApp()
app.mainloop()
这段就基本把一个“桌面小工具”的骨架搭出来了:上面标题,中间 Tab,多页面功能,各自独立逻辑。代码量不算多,但可扩展空间很大。
几个实战里比较容易踩坑的小点
这块简单口头说一下,你写的时候可以留个心眼:
主窗口一定要用 CTk(),不要混用Tk(),不然风格会很怪不要同一个容器里同时 pack和grid,选一个用就行文本相关的组件( CTkTextbox)记得用"1.0"/"end"这样的索引操作做耗时操作(比如文件扫描、网络请求),记得不要直接在主线程里跑,不然整个界面会“卡死”,可以考虑用 threading或者after()分步处理如果你准备打包成 exe,照常用 pyinstaller,CustomTkinter 这一层基本不用特殊处理,真出问题就多加--add-data把主题文件夹带进去
差不多就先聊到这儿,你可以先把上面这几个 demo 跑一遍,然后把自己的小脚本慢慢迁过来:把 Tk 换成 CTk,控件名前面加个 C,顺手加一点颜色和布局优化,你会发现本地小工具“观感升级”这件事,其实并不难。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB