Python技术迷

精通 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 ctk

ctk.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_theme
  • Frame + grid 做布局,让控件看起来“不挤”
  • CTkEntry 的 placeholder_text、show="*" 这种小细节
  • CTkCheckBox + BooleanVar 做状态绑定
  • CTkSwitch 直接实现暗黑模式切换
  • 键盘事件 <Return> 绑定登录

写业务的时候,基本就围着这几个组件打转,再配合一点请求逻辑,就可以跑去连后端了。

多页界面:用 Tabview 写一个小工具面板

很多桌面小工具都喜欢用“多标签页”来分功能,比如“配置”“日志”“关于”等。CustomTkinter 里直接用 CTkTabview 来搞:

import customtkinter as ctk

ctk.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