别再硬扛原生 Tkinter:CustomTkinter 的主题、Grid 与布局边界
源微信页常触发验证码拦截。正文不以那篇「精通 / 轻松构建」软文为底稿,而是按官方文档 customtkinter.tomschimansky.com 与仓库 TomSchimansky/CustomTkinter 重组:把外观模式、色板主题、Grid 权重、类继承结构讲清楚,并给出可本机运行的示例。目标不是「换个皮肤按钮」,而是让你在扩侧边栏、切深色模式、自定义 JSON 主题时不踩布局与主题边界。
CustomTkinter = 基于 Tkinter 的现代控件库。API 仍是「master + geometry manager」,但默认观感跨 Windows / macOS / Linux 更一致,并支持 HighDPI。

一、先分清两件事:外观模式 ≠ 色板主题
很多人把 set_appearance_mode 和 set_default_color_theme 混成一句「换主题」。官方语义是分开的:
| 概念 | API | 取值 | 管什么 |
|---|---|---|---|
| 外观模式 | set_appearance_mode(...) |
"System" / "Light" / "Dark" |
整窗亮暗;控件按当前模式从颜色元组里挑一边 |
| 色板主题 | set_default_color_theme(...) |
"blue" / "dark-blue" / "green" 或自定义 JSON 路径 |
默认 fg_color / hover_color 等色板 |
必须在创建任何控件之前调用二者。创建窗口之后再改主题,已存在控件不会自动换一套 JSON 色板;外观模式切换则相对友好——因为颜色可以写成 (浅色, 深色) 元组,控件会按当前模式自动切换。
import customtkinter as ctk
ctk.set_appearance_mode("System") # System | Light | Dark
ctk.set_default_color_theme("blue") # blue | dark-blue | green
app = ctk.CTk()
app.title("hello ctk")
app.geometry("420x160")
btn = ctk.CTkButton(app, text="点我", command=lambda: print("ok"))
btn.pack(padx=24, pady=24)
app.mainloop()
安装:
pip install customtkinter
# 升级
pip install customtkinter --upgrade
颜色参数边界(官方 Color 文档):
- 单色名 / 单 hex:亮暗模式共用同一色
- 元组
("#浅", "#深"):按外观模式自动选边 fg_color才是控件主体色;bg_color只是圆角背后的「露底」色,别把两者搞反
# 亮/暗各一套红色,随外观模式切换
btn = ctk.CTkButton(app, fg_color=("#DB3E39", "#821D1A"), text="危险操作")
二、布局红线:优先 Grid,慎用 place
官方教程明确建议:能用 grid 就别用 place;小 demo 可用 pack,可扩展界面一律 grid。
原因很现实:place(x,y) 在加「顶部再插一行」时,下面所有绝对坐标都要手改;grid / pack 只声明相对关系与 padding,窗口缩放时靠 weight 与 sticky 自动分配空间。
1. weight 与 sticky 是一对
空列/空行会塌缩到内容宽度。想让按钮随窗拉伸,至少做两步:
app.grid_columnconfigure(0, weight=1)
button.grid(row=0, column=0, padx=20, pady=20, sticky="ew")
weight>0 让格子参与剩余空间分配;sticky="ew" 让控件贴住格子东西两侧。只设 width= 而不配 weight,窗口变大时中间往往是大片空白。
2. 跨列与不等边距
columnspan / rowspan 跨格;padx / pady 可传元组表示上下或左右不等距:
import customtkinter as ctk
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("dark-blue")
def button_callback():
print("button pressed")
app = ctk.CTk()
app.title("grid demo")
app.geometry("420x180")
app.grid_columnconfigure((0, 1), weight=1)
btn = ctk.CTkButton(app, text="跨两列按钮", command=button_callback)
btn.grid(row=0, column=0, padx=20, pady=20, sticky="ew", columnspan=2)
cb1 = ctk.CTkCheckBox(app, text="选项 A")
cb1.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="w")
cb2 = ctk.CTkCheckBox(app, text="选项 B")
cb2.grid(row=1, column=1, padx=20, pady=(0, 20), sticky="w")
app.mainloop()
同一父容器内不要混用 pack 与 grid,否则 Tk 会直接报错。这是原生 Tk 规则,CustomTkinter 同样继承。


三、用类继承 CTk / CTkFrame,而不是脚本堆控件
官方 caution:除极小 demo 外,主窗继承 CTk,分区继承 CTkFrame / CTkToplevel。好处是:
1. __init__ 里布局一目了然,可拆多文件
2. 回调写成方法,避免闭包抓错变量
3. 侧边栏、内容区可各自成类,互不抢 grid 行列
下面是一个「左侧导航 + 右侧内容」的可运行骨架(完整文件见 practice/sidebar_app.py):
import customtkinter as ctk
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("green")
class Sidebar(ctk.CTkFrame):
def __init__(self, master, on_nav):
super().__init__(master, width=180, corner_radius=0)
self.grid_rowconfigure(4, weight=1)
ctk.CTkLabel(self, text="工具箱", font=ctk.CTkFont(size=18, weight="bold")).grid(
row=0, column=0, padx=16, pady=(20, 12), sticky="w"
)
for i, name in enumerate(("仪表盘", "任务", "设置"), start=1):
ctk.CTkButton(self, text=name, command=lambda n=name: on_nav(n)).grid(
row=i, column=0, padx=12, pady=6, sticky="ew"
)
self.mode = ctk.CTkOptionMenu(
self, values=["System", "Light", "Dark"],
command=lambda m: ctk.set_appearance_mode(m),
)
self.mode.grid(row=5, column=0, padx=12, pady=16, sticky="s")
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("CustomTkinter 侧边栏示例")
self.geometry("720x420")
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
self.sidebar = Sidebar(self, on_nav=self.show_page)
self.sidebar.grid(row=0, column=0, sticky="nsw")
self.content = ctk.CTkFrame(self, corner_radius=0)
self.content.grid(row=0, column=1, sticky="nsew")
self.content.grid_columnconfigure(0, weight=1)
self.content.grid_rowconfigure(1, weight=1)
self.heading = ctk.CTkLabel(self.content, text="仪表盘", font=ctk.CTkFont(size=22, weight="bold"))
self.heading.grid(row=0, column=0, padx=24, pady=(24, 8), sticky="w")
self.body = ctk.CTkTextbox(self.content)
self.body.grid(row=1, column=0, padx=24, pady=(0, 24), sticky="nsew")
self.body.insert("0.0", "在左侧切换页面;底部 OptionMenu 可改外观模式。\n")
def show_page(self, name: str):
self.heading.configure(text=name)
self.body.delete("0.0", "end")
self.body.insert("0.0", f"当前页:{name}\n把业务面板挂到这里即可。\n")
if __name__ == "__main__":
App().mainloop()
注意侧边栏设了固定 width=180 且 corner_radius=0:侧栏通常不参与 weight 拉伸,内容列 weight=1。若给侧栏也 weight=1,导航会被拉得很宽,观感反而变差——这是布局边界,不是 bug。
四、控件层:常用组件与配置入口
CustomTkinter 控件命名统一 CTk*,用法接近 Tk,但样式参数更完整。以 CTkButton 为例,高频参数包括:
| 参数 | 作用 |
|---|---|
fg_color / hover_color / border_color |
主体 / 悬停 / 边框色(可元组) |
corner_radius / border_width |
圆角与描边 |
state |
"normal" / "disabled" |
command |
点击回调 |
configure / cget |
运行时改读属性 |
invoke |
程序触发点击逻辑 |
其它常用控件:CTkLabel、CTkEntry、CTkCheckBox、CTkSwitch、CTkSlider、CTkOptionMenu、CTkComboBox、CTkTextbox、CTkScrollableFrame、CTkTabview、CTkProgressBar。
可滚动容器是真实业务里的刚需:列表项一多,普通 CTkFrame 装不下。CTkScrollableFrame 支持垂直/水平方向,内部仍用 grid/pack 摆子控件。
运行时改文案:
button.configure(text="已提交", state="disabled")
print(button.cget("text"))

五、自定义主题 JSON:全局换肤的正确姿势
内置三套色板(blue / dark-blue / green)不够时,复制官方主题 JSON,改各控件的默认色,再:
ctk.set_default_color_theme("/path/to/your_theme.json")
边界提醒:
1. 路径在创建控件前生效;中途 set_default_color_theme 不会 magically 重绘整棵控件树。
2. JSON 里每个控件通常同时写 light / dark 字段;漏写会导致某一模式下颜色回退难看。
3. 单控件仍可用 fg_color=(...) 覆盖主题默认值——适合「全局绿、危险按钮红」。
4. 主题只管颜色默认值,不管布局。布局崩了去查 grid_rowconfigure / sticky,别在 JSON 里找答案。
外观模式可在运行时切换(如上例 CTkOptionMenu);色板主题更适合「启动时选定品牌色」。产品若要「用户自选皮肤」,更稳妥的是自己维护几套 JSON,重启或重建顶层窗口后加载,而不是指望热切换 JSON。
六、排坑清单与选型边界
常见坑
1. 同一 master 混用 pack/grid → TclError。分区用不同 Frame,每个 Frame 内部统一一种几何管理器。
2. 忘记 weight → 窗口拉大后控件挤在左上角。内容区行列至少有一个 weight=1。
3. 用 place 堆复杂页 → 短期快、长期改不动。官方直接劝退。
4. 脚本式 300 行控件 → 难测难拆。主窗 / 侧栏 / 弹窗分家。
5. 在无显示环境跑 GUI → CI / Docker 无 X11 时 mainloop 起不来;冒烟测可只 import customtkinter,真 UI 测用带桌面的 runner 或截图机。
6. 线程里直接改控件 → 与 Tk 一样,UI 更新应回到主线程(after 调度)。
什么时候用 / 不用
| 场景 | 建议 |
|---|---|
| 内部工具、脚本加壳、跨平台小桌面 | CustomTkinter 很合适 |
| 要原生平台菜单/复杂文档编辑 | 评估 Qt / wx / 原生框架 |
| Web 优先、要远程访问 | 别硬上桌面 GUI |
| 已有大量经典 Tk 代码 | 可渐进混用 CTk 控件与 Tk 控件,但样式统一要自己收口 |
七、最小验证步骤
pip install customtkinter
python practice/hello_ctk.py
python practice/sidebar_app.py
看到现代圆角按钮、深浅色可切换、侧栏导航能改右侧标题,就说明安装与布局骨架正确。再往业务里填:CTkTabview 做设置页、CTkProgressBar 接后台任务、CTkScrollableFrame 承载动态列表。
仓库与文档:
把「外观模式 / 色板主题 / Grid 权重 / 类结构」四条边界守住,CustomTkinter 就不再是「换皮 Tk」,而是一套能跟着工具需求长的桌面 UI 底盘。
💻 配套实训环境与动手练习
本文涉及的相关技术指令、开发环境与工具链已内置在边学边练在线实验室中,无需繁琐安装配置,随时在浏览器中实践体验: