2026-05-21
Python
0

目录

CustomTkinter 入门小项目:构建一个简单的设备控制面板(完整示例)
🤔 先聊聊,为什么是 CustomTkinter?
🛠️ 环境准备
📐 面板设计思路
💻 完整代码实现
🔍 几个值得注意的细节
🚀 可以继续扩展的方向
📌 小结

CustomTkinter 入门小项目:构建一个简单的设备控制面板(完整示例)


🤔 先聊聊,为什么是 CustomTkinter?

做 Python 桌面开发,很多人第一反应是 Tkinter——毕竟内置、免安装、上手快。但打开一看,那个界面……说实话,像是从 Windows 98 穿越过来的。

用 PyQt?功能强,可学习曲线陡得像悬崖。用 wxPython?文档读起来比说明书还费劲。

CustomTkinter 刚好卡在中间。它基于原生 Tkinter 构建,但把所有控件重新包了一层,支持深色/浅色主题切换,圆角按钮、现代配色开箱即用。对于工控、自动化、内部工具这类场景,完全够用——而且学一下午就能上手。

今天咱们就用它做一个设备控制面板:模拟几台设备的开关控制、状态显示和日志记录。代码完整可运行,Windows 下直接跑。


🛠️ 环境准备

先把依赖装好,一行命令搞定:

bash
pip install customtkinter

Python 版本建议 3.9 及以上。CustomTkinter 不依赖任何第三方 GUI 库,底层还是 Tkinter,所以 Windows 上不需要额外折腾。

验证一下安装:

python
import customtkinter print(customtkinter.__version__)

能打印出版本号就 OK 了。


📐 面板设计思路

在动手写代码之前,先把结构想清楚。这个控制面板要实现:

  • 顶部:标题栏 + 主题切换按钮
  • 中部左侧:设备列表,每台设备有独立的开/关按钮和状态指示
  • 中部右侧:实时日志区域,记录操作历史
  • 底部:全局操作按钮(一键全开、一键全关)

整体布局用 grid 管理,比 pack 更好控制对齐。设备数据用字典维护状态,不搞复杂的类继承——够用就好。


💻 完整代码实现

直接上代码,每个关键部分都有注释说明:

python
import 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()

image.png

image.png

image.png


🔍 几个值得注意的细节

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 把设备状态轮询放到后台线程,避免界面卡顿
  • 把设备配置抽成 JSON 文件,动态加载,不用改代码就能增减设备
  • 加一个 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 许可协议。转载请注明出处!