做 Python 桌面开发,很多人第一反应是 Tkinter——毕竟内置、免安装、上手快。但打开一看,那个界面……说实话,像是从 Windows 98 穿越过来的。
用 PyQt?功能强,可学习曲线陡得像悬崖。用 wxPython?文档读起来比说明书还费劲。
CustomTkinter 刚好卡在中间。它基于原生 Tkinter 构建,但把所有控件重新包了一层,支持深色/浅色主题切换,圆角按钮、现代配色开箱即用。对于工控、自动化、内部工具这类场景,完全够用——而且学一下午就能上手。
今天咱们就用它做一个设备控制面板:模拟几台设备的开关控制、状态显示和日志记录。代码完整可运行,Windows 下直接跑。
先把依赖装好,一行命令搞定:
bashpip install customtkinter
Python 版本建议 3.9 及以上。CustomTkinter 不依赖任何第三方 GUI 库,底层还是 Tkinter,所以 Windows 上不需要额外折腾。
验证一下安装:
pythonimport customtkinter
print(customtkinter.__version__)
能打印出版本号就 OK 了。
在动手写代码之前,先把结构想清楚。这个控制面板要实现:
整体布局用 grid 管理,比 pack 更好控制对齐。设备数据用字典维护状态,不搞复杂的类继承——够用就好。
直接上代码,每个关键部分都有注释说明:
pythonimport customtkinter as ctk
from datetime import datetime
# ── 全局主题设置 ──────────────────────────────────────────────
ctk.set_appearance_mode("dark") # 默认深色主题
ctk.set_default_color_theme("blue") # 蓝色主题色
class DeviceControlPanel(ctk.CTk):
"""设备控制面板主窗口"""
def __init__(self):
super().__init__()
self.title("设备控制面板 - CustomTkinter 示例")
self.geometry("860x580")
self.resizable(False, False)
# 设备数据:名称 -> 当前状态(True=运行中,False=已停止)
self.devices = {
"传送带电机 A": False,
"液压泵 B": False,
"冷却风扇 C": False,
"加热模块 D": False,
"视觉检测仪 E": False,
}
# 存储每个设备对应的按钮和状态标签,方便后续更新
self.device_buttons = {}
self.device_labels = {}
self._build_ui()
self._log("系统初始化完成,所有设备待机中。")
# ── UI 构建 ────────────────────────────────────────────────
def _build_ui(self):
"""搭建整体布局"""
self.grid_columnconfigure(0, weight=3)
self.grid_columnconfigure(1, weight=2)
self.grid_rowconfigure(1, weight=1)
self._build_header()
self._build_device_panel()
self._build_log_panel()
self._build_footer()
def _build_header(self):
"""顶部标题栏"""
header_frame = ctk.CTkFrame(self, height=60, corner_radius=0)
header_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
header_frame.grid_propagate(False)
title_label = ctk.CTkLabel(
header_frame,
text="⚙ 设备控制面板",
font=ctk.CTkFont(size=20, weight="bold"),
)
title_label.pack(side="left", padx=20, pady=10)
# 主题切换按钮,放在右上角
theme_btn = ctk.CTkButton(
header_frame,
text="切换主题",
width=90,
height=32,
command=self._toggle_theme,
)
theme_btn.pack(side="right", padx=20, pady=10)
def _build_device_panel(self):
"""左侧设备控制区"""
device_frame = ctk.CTkFrame(self)
device_frame.grid(row=1, column=0, padx=(10, 5), pady=10, sticky="nsew")
section_label = ctk.CTkLabel(
device_frame,
text="设备列表",
font=ctk.CTkFont(size=14, weight="bold"),
)
section_label.pack(anchor="w", padx=15, pady=(12, 6))
# 分割线(用细 Frame 模拟)
ctk.CTkFrame(device_frame, height=1, fg_color="gray40").pack(
fill="x", padx=15, pady=(0, 8)
)
# 逐个渲染设备行
for device_name in self.devices:
self._create_device_row(device_frame, device_name)
def _create_device_row(self, parent, device_name: str):
"""
为每台设备创建一行控件:
[设备名称] [状态标签] [操作按钮]
"""
row_frame = ctk.CTkFrame(parent, fg_color="transparent")
row_frame.pack(fill="x", padx=15, pady=4)
# 设备名称
name_label = ctk.CTkLabel(
row_frame,
text=device_name,
width=160,
anchor="w",
font=ctk.CTkFont(size=13),
)
name_label.pack(side="left")
# 状态标签(初始为"已停止",红色)
status_label = ctk.CTkLabel(
row_frame,
text="● 已停止",
text_color="#FF6B6B",
width=80,
font=ctk.CTkFont(size=12),
)
status_label.pack(side="left", padx=(10, 0))
# 操作按钮
btn = ctk.CTkButton(
row_frame,
text="启 动",
width=80,
height=30,
fg_color="#2ECC71",
hover_color="#27AE60",
command=lambda name=device_name: self._toggle_device(name),
)
btn.pack(side="right")
# 记录引用,后续更新用
self.device_buttons[device_name] = btn
self.device_labels[device_name] = status_label
def _build_log_panel(self):
"""右侧日志区"""
log_frame = ctk.CTkFrame(self)
log_frame.grid(row=1, column=1, padx=(5, 10), pady=10, sticky="nsew")
log_title = ctk.CTkLabel(
log_frame,
text="操作日志",
font=ctk.CTkFont(size=14, weight="bold"),
)
log_title.pack(anchor="w", padx=15, pady=(12, 6))
ctk.CTkFrame(log_frame, height=1, fg_color="gray40").pack(
fill="x", padx=15, pady=(0, 8)
)
# 日志文本框,只读
self.log_box = ctk.CTkTextbox(
log_frame,
font=ctk.CTkFont(size=11, family="Consolas"),
state="disabled", # 禁止用户直接编辑
)
self.log_box.pack(fill="both", expand=True, padx=10, pady=(0, 10))
# 清空日志按钮
clear_btn = ctk.CTkButton(
log_frame,
text="清空日志",
height=30,
fg_color="gray30",
hover_color="gray40",
command=self._clear_log,
)
clear_btn.pack(padx=10, pady=(0, 10), fill="x")
def _build_footer(self):
"""底部全局操作栏"""
footer_frame = ctk.CTkFrame(self, height=55, corner_radius=0)
footer_frame.grid(row=2, column=0, columnspan=2, sticky="ew")
footer_frame.grid_propagate(False)
# 全部启动
start_all_btn = ctk.CTkButton(
footer_frame,
text="▶ 全部启动",
width=140,
height=36,
fg_color="#2ECC71",
hover_color="#27AE60",
command=self._start_all,
)
start_all_btn.pack(side="left", padx=20, pady=9)
# 全部停止
stop_all_btn = ctk.CTkButton(
footer_frame,
text="■ 全部停止",
width=140,
height=36,
fg_color="#E74C3C",
hover_color="#C0392B",
command=self._stop_all,
)
stop_all_btn.pack(side="left", padx=(0, 20), pady=9)
# 右下角状态统计
self.summary_label = ctk.CTkLabel(
footer_frame,
text="运行中:0 / 5",
font=ctk.CTkFont(size=12),
text_color="gray70",
)
self.summary_label.pack(side="right", padx=20)
# ── 业务逻辑 ───────────────────────────────────────────────
def _toggle_device(self, device_name: str):
"""单台设备开/关切换"""
current_state = self.devices[device_name]
new_state = not current_state
self.devices[device_name] = new_state
self._update_device_ui(device_name, new_state)
action = "启动" if new_state else "停止"
self._log(f"[{device_name}] 已{action}")
self._update_summary()
def _update_device_ui(self, device_name: str, is_running: bool):
"""根据状态刷新按钮和标签的视觉效果"""
btn = self.device_buttons[device_name]
label = self.device_labels[device_name]
if is_running:
btn.configure(
text="停 止",
fg_color="#E74C3C",
hover_color="#C0392B",
)
label.configure(text="● 运行中", text_color="#2ECC71")
else:
btn.configure(
text="启 动",
fg_color="#2ECC71",
hover_color="#27AE60",
)
label.configure(text="● 已停止", text_color="#FF6B6B")
def _start_all(self):
"""一键启动所有设备"""
for name in self.devices:
if not self.devices[name]:
self.devices[name] = True
self._update_device_ui(name, True)
self._log("── 全部设备已启动 ──")
self._update_summary()
def _stop_all(self):
"""一键停止所有设备"""
for name in self.devices:
if self.devices[name]:
self.devices[name] = False
self._update_device_ui(name, False)
self._log("── 全部设备已停止 ──")
self._update_summary()
def _update_summary(self):
"""刷新底部运行统计"""
running_count = sum(1 for v in self.devices.values() if v)
total = len(self.devices)
self.summary_label.configure(text=f"运行中:{running_count} / {total}")
def _toggle_theme(self):
"""深色/浅色主题切换"""
current = ctk.get_appearance_mode()
new_mode = "light" if current == "Dark" else "dark"
ctk.set_appearance_mode(new_mode)
self._log(f"主题已切换为:{'浅色' if new_mode == 'light' else '深色'}")
# ── 日志操作 ───────────────────────────────────────────────
def _log(self, message: str):
"""向日志框追加一条带时间戳的记录"""
timestamp = datetime.now().strftime("%H:%M:%S")
log_line = f"[{timestamp}] {message}\n"
self.log_box.configure(state="normal")
self.log_box.insert("end", log_line)
self.log_box.see("end") # 自动滚动到底部
self.log_box.configure(state="disabled")
def _clear_log(self):
"""清空日志内容"""
self.log_box.configure(state="normal")
self.log_box.delete("1.0", "end")
self.log_box.configure(state="disabled")
self._log("日志已清空。")
# ── 入口 ──────────────────────────────────────────────────────
if __name__ == "__main__":
app = DeviceControlPanel()
app.mainloop()



grid_propagate(False) 这行别漏掉。 顶部和底部的 Frame 设了固定高度,如果不加这句,子控件会把父容器撑变形,布局就乱了。这是 Tkinter 系里一个经典的"坑",新手很容易在这里卡住半天。
CTkTextbox 的只读控制,不是靠什么 readonly 属性,而是通过 state="disabled" 和 state="normal" 来回切换实现的。写入前先 normal,写完再 disabled——记住这个节奏,否则要么写不进去,要么用户能随意编辑。
lambda name=device_name 这里用了默认参数绑定。直接写 lambda: self._toggle_device(device_name) 的话,循环结束后所有按钮都会绑定到最后一个设备名——Python 闭包的经典陷阱,这里必须显式绑定。
这个面板是个骨架,实际项目里可以往上叠很多东西。比如:
pyserial),让按钮真正控制硬件threading 把设备状态轮询放到后台线程,避免界面卡顿CTkProgressBar 显示设备负载百分比,视觉上更直观CustomTkinter 的控件还有很多没用到——CTkTabview(标签页)、CTkScrollableFrame(可滚动容器)、CTkSlider(滑块)——等设备多了、功能复杂了,这些自然就用上了。
CustomTkinter 的核心价值就一句话:用最低的学习成本,换来一个说得过去的现代界面。它不是做商业软件的最终选择,但对于内部工具、快速原型、工控辅助面板这类场景,真的够用,而且比折腾 Qt 省心得多。
这个控制面板示例大概 200 行代码,涵盖了布局管理、状态维护、事件绑定、日志系统这几个最常见的需求点。把它跑起来,改改设备名,换换颜色,慢慢就能摸清 CustomTkinter 的脾气了。
标签:#Python桌面开发 #CustomTkinter #GUI开发 #工控软件 #Python实战
相关信息
我用夸克网盘给你分享了「controlPanelDemo.zip」,点击链接或复制整段内容,打开「夸克APP」即可获取。
/72103YgIF2:/
链接:https://pan.quark.cn/s/6ed7977e0f24
提取码:Em1s
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!