找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

635

积分

0

好友

79

主题
发表于 5 天前 | 查看: 27| 回复: 0

我先直接说结论哈:如果你平时写 Python 小工具,总嫌 Tkinter 太丑、太上古,那真可以试试 CustomTkinter,学习成本几乎一样,但颜值直接上一个档次,不用你自己画控件、写一堆样式。

我前两天在公司加班,晚上九点多,人有点迷糊,想给测试同事写个小工具:批量改接口环境的配置,本来打算整一个命令行脚本,结果对方说一句:“能不能点点按钮的那种,我不想记参数。”

你要是直接上 Tkinter,默认那种灰扑扑的窗口+古早风按钮,给同事看一眼,大概率会被吐槽“这啥年代的界面”。但你要上 PyQt / Qt,又太重,打包、依赖、体积都不太友好。

这时候 CustomTkinter 就很适合:还是 Tkinter 的那套机制,但控件全是现代风 UI,自带暗黑模式、主题颜色,代码量变化不大。

先跑起来:第一个 CustomTkinter 窗口

先别急着看大项目,先搞一个能跑、能点的窗口出来,心里有点底。

安装就一行,环境里有 pip 就行:

pip install customtkinter

然后来个最小可用 Demo,我用的是比较常见的写法,把 CustomTkinter 简写成 ctk:

import customtkinter as ctk

# 全局外观设置(可选)
ctk.set_appearance_mode("dark")        # "light" / "dark" / "system"
ctk.set_default_color_theme("blue")    # "blue" / "green" / "dark-blue"

# 创建主窗口
app = ctk.CTk()
app.title("CustomTkinter Demo")
app.geometry("400x250")  # 窗口大小

# 一个标签
label = ctk.CTkLabel(app, text="点下面的按钮试试:")
label.pack(pady=20)

# 点击计数
count = 0

def on_click():
    nonlocal count  # Python 3.8+ 可以这么用
    count += 1
    label.configure(text=f"你点了 {count} 下啦")

# 一个按钮
btn = ctk.CTkButton(app, text="点我", command=on_click)
btn.pack(pady=10)

# 一个暗黑/明亮模式切换
def toggle_mode():
    current = ctk.get_appearance_mode()
    if current == "Dark":
        ctk.set_appearance_mode("light")
    else:
        ctk.set_appearance_mode("dark")

switch = ctk.CTkSwitch(app, text="切换明暗模式", command=toggle_mode)
switch.pack(pady=10)

app.mainloop()

你跑一下就知道它和原生 Tkinter 差别在哪儿了:同样的大小、同样的布局,但按钮、字体、背景都是现代风 UI,而且还支持暗黑模式切换,这个在 Tkinter 原生里自己搞会很费劲。

有几个点顺带说一下:

  • CTk() 就是 CustomTkinter 的主窗口,等价于 Tkinter 里的 tk.Tk()
  • 所有控件基本都前面多了个 C:CTkLabelCTkButtonCTkEntryCTkTextbox...
  • set_appearance_modeset_default_color_theme 是全局的,最好在创建窗口之前调用。

做个完整一点的小工具:待办清单 ToDo

光一个按钮没意思,我们搞个非常实用但不复杂的小东西:一个小型 ToDo 应用,能:

  • 输入任务
  • 点击添加后显示在列表里
  • 支持勾选“已完成”,自动加删除线或标记
  • 右侧有个清理已完成的按钮

功能不复杂,但把常用的控件、布局、回调都串联起来了。

先来一个相对完整的版本,你可以直接复制跑:

import customtkinter as ctk
from datetime import datetime

ctk.set_appearance_mode(“dark”)
ctk.set_default_color_theme(“dark-blue”)

class TodoApp(ctk.CTk):

    def __init__(self):
        super().__init__()

        self.title(“CustomTkinter 待办清单”)
        self.geometry(“600x400”)

        # 整体分左右两块:左边主区域,右边工具栏
        self.columnconfigure(0, weight=3)
        self.columnconfigure(1, weight=1)
        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=0)

        # ===== 顶部标题 =====
        title = ctk.CTkLabel(self, text=“今天要做啥?”, font=ctk.CTkFont(size=20, weight=“bold”))
        title.grid(row=0, column=0, columnspan=2, sticky=“w”, padx=20, pady=(15, 5))

        # ===== 中间任务列表(可滚动区域)=====
        self.task_frame = ctk.CTkScrollableFrame(self, label_text=“待办列表”)
        self.task_frame.grid(row=1, column=0, sticky=“nsew”, padx=20, pady=10)

        self.task_frame.columnconfigure(0, weight=1)

        self.tasks = []  # 记录所有任务控件

        # ===== 右侧工具栏 =====
        toolbar = ctk.CTkFrame(self)
        toolbar.grid(row=1, column=1, sticky=“ns”, padx=(0, 20), pady=10)

        # 输入框
        self.entry = ctk.CTkEntry(toolbar, width=180, placeholder_text=“输入任务内容...”)
        self.entry.pack(pady=(10, 5))

        # 添加按钮
        add_btn = ctk.CTkButton(toolbar, text=“添加任务”, command=self.add_task)
        add_btn.pack(pady=5)

        # 清理已完成
        clear_btn = ctk.CTkButton(toolbar, text=“清理已完成”, fg_color=”#a83232",
                                  hover_color=”#7a2525”, command=self.clear_done)
        clear_btn.pack(pady=(20, 5))

        # 模式切换
        self.mode_switch = ctk.CTkSwitch(toolbar, text=“暗黑模式”, command=self.toggle_mode)
        self.mode_switch.select()  # 默认选中 = dark
        self.mode_switch.pack(pady=(20, 5))

        # 状态栏
        self.status_label = ctk.CTkLabel(self, text=“欢迎~”, anchor=“w”)
        self.status_label.grid(row=2, column=0, columnspan=2, sticky=“we”, padx=20, pady=(0, 10))

        # 输入框回车直接添加
        self.entry.bind(“<Return>”, lambda event: self.add_task())

    def add_task(self):
        text = self.entry.get().strip()
        if not text:
            self.set_status(“内容为空,不添加。”)
            return

        # 单行:左边一个勾选框 + 右边时间
        row = len(self.tasks)
        frame = ctk.CTkFrame(self.task_frame)
        frame.grid(row=row, column=0, sticky=“we”, pady=2, padx=5)
        frame.columnconfigure(0, weight=1)

        var = ctk.BooleanVar(value=False)

        checkbox = ctk.CTkCheckBox(
            frame,
            text=text,
            variable=var,
            onvalue=True,
            offvalue=False,
            command=lambda: self.on_task_check(checkbox, var)
        )
        checkbox.grid(row=0, column=0, sticky=“w”)

        time_label = ctk.CTkLabel(frame, text=datetime.now().strftime(“%H:%M”))
        time_label.grid(row=0, column=1, sticky=“e”, padx=(10, 5))

        self.tasks.append((frame, checkbox, var))

        self.entry.delete(0, “end”)
        self.set_status(f“添加任务:{text}”)

    def on_task_check(self, checkbox, var):
        # 勾选后给任务做个简单标记
        if var.get():
            checkbox.configure(text=f“✅ {checkbox.cget(‘text’)}”)
        else:
            txt = checkbox.cget(“text”)
            if txt.startswith(“✅ ”):
                checkbox.configure(text=txt[2:])
        self.update_status_counts()

    def clear_done(self):
        # 把已经选择的任务删掉
        remain = []
        removed = 0
        for frame, checkbox, var in self.tasks:
            if var.get():
                frame.destroy()
                removed += 1
            else:
                remain.append((frame, checkbox, var))

        self.tasks = remain

        # 重新排一下行号
        for idx, (frame, _, _) in enumerate(self.tasks):
            frame.grid_configure(row=idx)

        self.set_status(f“清理已完成任务 {removed} 条”)
        self.update_status_counts()

    def update_status_counts(self):
        total = len(self.tasks)
        done = sum(1 for _, _, v in self.tasks if v.get())
        self.status_label.configure(text=f“总共 {total} 条,已完成 {done} 条”)

    def toggle_mode(self):
        if self.mode_switch.get():
            ctk.set_appearance_mode(“dark”)
            self.mode_switch.configure(text=“暗黑模式”)
        else:
            ctk.set_appearance_mode(“light”)
            self.mode_switch.configure(text=“亮色模式”)

    def set_status(self, msg: str):
        self.status_label.configure(text=msg)

if __name__ == “__main__”:
    app = TodoApp()
    app.mainloop()

这个小例子里,其实已经用到 CustomTkinter 很多日常会遇到的东西:

  • CTkScrollableFrame 做滚动区域,比自己绑 Scrollbar 省事多了。
  • CTkCheckBox + BooleanVar 管理状态,很符合 Tk 的一贯风格。
  • grid 布局配合 rowconfigure/columnconfigure,窗口缩放时会自适应。
  • 小小的 status_label 做状态提示,这个在实际工具里特别有用。

你可以先跑一遍,熟悉一下窗体布局、控件创建的节奏。

再往里一点:常用控件和布局的小习惯

你以后用 CustomTkinter 写东西,七成时间就围着几类控件打转:

  • 文本类:CTkLabelCTkTextbox(多行输入)、CTkEntry(单行)
  • 按钮类:CTkButtonCTkSwitchCTkCheckBox
  • 选择类:CTkOptionMenuCTkComboBox(新版支持)
  • 进度类:CTkProgressBarCTkSlider
  • 容器类:CTkFrameCTkScrollableFrame, 再加 CTkTabview

随便举个“下载进度条”风格的小 UI,给你看看进度条和滑块用法:

import customtkinter as ctk
import time
import threading

ctk.set_appearance_mode(“dark”)
ctk.set_default_color_theme(“green”)

class DownloadDemo(ctk.CTk):

    def __init__(self):
        super().__init__()

        self.title(“下载进度 Demo”)
        self.geometry(“400x220”)

        self.label = ctk.CTkLabel(self, text=“准备就绪”, font=ctk.CTkFont(size=16))
        self.label.pack(pady=(25, 10))

        self.progress = ctk.CTkProgressBar(self)
        self.progress.set(0)
        self.progress.pack(fill=“x”, padx=40, pady=10)

        self.speed_label = ctk.CTkLabel(self, text=“模拟下载速度:1x”)
        self.speed_label.pack(pady=(10, 0))

        self.speed_var = ctk.DoubleVar(value=1.0)
        self.speed_slider = ctk.CTkSlider(
            self,
            from_=0.5,
            to=3.0,
            number_of_steps=5,
            variable=self.speed_var,
            command=self.on_speed_change
        )
        self.speed_slider.pack(fill=“x”, padx=40, pady=5)

        self.btn = ctk.CTkButton(self, text=“开始下载”, command=self.start_download)
        self.btn.pack(pady=10)

        self._downloading = False

    def on_speed_change(self, value):
        self.speed_label.configure(text=f“模拟下载速度:{value:.1f}x”)

    def start_download(self):
        if self._downloading:
            return
        self._downloading = True
        self.btn.configure(state=“disabled”)
        self.label.configure(text=“下载中...”)

        threading.Thread(target=self._download_task, daemon=True).start()

    def _download_task(self):
        progress = 0.0
        while progress < 1.0:
            time.sleep(0.1 / self.speed_var.get())
            progress += 0.02
            # UI 更新要丢回主线程,CustomTkinter 这里小范围直接用 after 也行
            self.after(0, lambda v=progress: self.progress.set(min(v, 1.0)))

        def done():
            self.label.configure(text=“下载完成 ✅”)
            self.btn.configure(state=“normal”)
            self._downloading = False

        self.after(0, done)

if __name__ == “__main__”:
    DownloadDemo().mainloop()

这里顺便踩了一个实战中经常会遇到的坑:长任务 (比如下载、计算、IO) 千万别直接在按钮回调里 time.sleep 或者跑大循环,会把整个 GUI 卡死。上面这个写法里,用了 threading.Thread + self.after,就是比较常见的组合:

  • 耗时逻辑丢给子线程,让主线程专注画界面、响应事件。
  • 子线程里每次进度变化,用 after(0, ...) 把 UI 更新交给主线程执行,避免跨线程直接操作控件。

这套关于多线程与异步任务处理的习惯你要是一开始就用对了,后面做工具的时候会省很多坑。

稍微上一个台阶:用类和模块把 GUI 收拾干净

很多人学 GUI 的时候,刚开始都是这样写:

app = ctk.CTk()

btn = ctk.CTkButton(app, ...)
btn.pack()

entry = ctk.CTkEntry(app, ...)
entry.pack()

# 一堆全局变量 + 函数
def on_click():
    ...

app.mainloop()

小 Demo 没问题,但稍微大一点就开始乱:变量到处飞、回调函数一大堆、跨模块调用很难写单元测试。

上面 ToDo 的例子,其实就是一个比较好的练习方向:把窗口本身建成一个类,把所有控件挂到 self 上,逻辑也变成这个类的方法,传递状态就简单很多。

再举个结构稍微清晰一点的骨架,你做项目的时候可以按这个套路来扩:

import customtkinter as ctk

class SettingsView(ctk.CTkFrame):
    “”“右侧设置区域”“”

    def __init__(self, master, on_env_change):
        super().__init__(master)

        self.on_env_change = on_env_change

        self.label = ctk.CTkLabel(self, text=“环境选择”)
        self.label.pack(pady=(10, 5))

        self.env_var = ctk.StringVar(value=“dev”)
        self.env_menu = ctk.CTkOptionMenu(
            self,
            values=[“dev”, “test”, “prod”],
            variable=self.env_var,
            command=self._env_changed
        )
        self.env_menu.pack(pady=5)

    def _env_changed(self, value):
        if self.on_env_change:
            self.on_env_change(value)

class MainView(ctk.CTkFrame):
    “”“左侧主操作区域”“”

    def __init__(self, master):
        super().__init__(master)

        self.info = ctk.CTkLabel(self, text=“当前环境:dev”)
        self.info.pack(pady=20)

        self.run_btn = ctk.CTkButton(self, text=“执行操作”, command=self.run)
        self.run_btn.pack(pady=10)

    def set_env(self, env: str):
        self.info.configure(text=f“当前环境:{env}”)

    def run(self):
        # 这里写你的实际业务逻辑
        print(“执行操作喽”)

class EnvToolApp(ctk.CTk):

    def __init__(self):
        super().__init__()

        self.title(“环境切换小工具”)
        self.geometry(“520x260”)

        self.columnconfigure(0, weight=2)
        self.columnconfigure(1, weight=1)
        self.rowconfigure(0, weight=1)

        self.main_view = MainView(self)
        self.main_view.grid(row=0, column=0, sticky=“nsew”, padx=(15, 5), pady=15)

        self.settings = SettingsView(self, on_env_change=self.on_env_change)
        self.settings.grid(row=0, column=1, sticky=“nsew”, padx=(5, 15), pady=15)

    def on_env_change(self, env):
        self.main_view.set_env(env)

if __name__ == “__main__”:
    ctk.set_appearance_mode(“system”)
    ctk.set_default_color_theme(“blue”)
    EnvToolApp().mainloop()

这段代码你以后可以当模板用:

  • 所有“区域”用 CTkFrame 子类表示(MainView、SettingsView)。
  • 主窗口负责把这些区域拼起来、做跨区域的事件配合。
  • 具体控件的细节封装在各自 View 里,调用的时候就不用到处找变量。

做复杂点的 GUI,比如多 Tab、多页面的配置工具,基本就是在这个骨架上不断加 View。

CustomTkinter 小技巧顺手说几个

这个话题再展开就太长了,我挑几个日常非常常用的点说一下,你后面自己查官方文档或者源码,可以玩得更花一点:

  1. 主题颜色自定义
    不想用内置的 “blue” / “green” / “dark-blue”,可以加载自定义 json 主题:

    ctk.set_default_color_theme(“my_theme.json”)

    json 里可以定义主色、按钮色、文字色等等,适合对 UI 比较较真的同学。

  2. 窗口图标 & 居中
    Tkinter 那一套也能照搬:

    app = ctk.CTk()
    app.iconbitmap(“xxx.ico”)  # Windows 下换图标
    
    # 简单居中一下
    w, h = 600, 400
    app.geometry(f“{w}x{h}+{(app.winfo_screenwidth()-w)//2}+{(app.winfo_screenheight()-h)//2}”)
  3. 和原生 Tkinter 混用(不太建议,但可以)
    CustomTkinter 是基于 Tkinter 做的,你确实可以在一个窗口里混合用一点 tk 原生控件,比如你想用 tk.Canvas 画点东西,基本上也跑得起来。但一般不推荐这样做,风格会不统一,能用 CustomTkinter 的控件尽量用它那套。

差不多就先聊到这儿,再写我得去泡杯咖啡续命了。你可以先把上面的 Demo 都跑一遍,然后拿其中一个小骨架,改成你自己现在手上需要的小工具,比如:

  • 快速切换 hosts / 环境配置
  • 小型接口调试面板
  • 本地脚本的参数面板(帮测试、运营点点按钮就能跑脚本)

写着写着,你就会发现:原来 Python 写个顺眼的 GUI,并没有想象中那么麻烦。


希望这篇从实战出发的指南能帮你快速上手 CustomTkinter。如果你想查看更多类似的 Python 工具开发技巧,欢迎访问 云栈社区 与我们交流。




上一篇:Go 紧急发布 1.25.6、1.24.12 安全补丁及 1.26 RC2 版本
下一篇:i3-6100U NAS搭建:69元机箱与250元总成本实现4K转码
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:49 , Processed in 0.365360 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表