上个月,老板突然把我叫到办公室。"小王啊,那个污水处理厂的监控系统,能不能先做个演示版?"我一愣——现场设备还没装呢,监控啥?但转念一想,这不正是展示技术实力的好机会嘛!
就这样,我用纯Python的Tkinter搞了个动态工业流程模拟器。水箱里的液体会流动、泵会转、阀门能手动开关、流量计的数字跳得比股票行情还欢实。演示那天,客户盯着屏幕看了五分钟,当场拍板:"就要这个效果!"
今天咱就聊聊,怎么用最朴素的Tkinter画出这么个玩意儿。不需要PyQt5那套重装备,更不用碰Unity3D(太杀鸡用牛刀了)。
很多人会问:市面上不是有组态软件吗?对。但你考虑过这几个现实问题没:
成本账:一套正经的组态软件授权费,少说也得五位数起步。我们这种演示项目,预算就三千块。
定制难:那些软件的界面模板固定得要命,想改个颜色都得翻半天手册。
依赖重:客户现场可能只有台老旧Win7电脑,你让我装个几百兆的运行环境?
用Tkinter就不一样了——Python自带的库,零额外依赖。代码写完直接打包成exe,扔到U盘里就能跑。关键是完全可控,想加啥动画效果随便折腾。
在动手之前,咱得先理清楚这套系统的核心机制。工业流程不是随便画几个图标就完事的,它得符合物理常识和逻辑因果。
水箱(储液) → 泵(动力) → 阀门(控制) → 流量计(监测) ↑____________反馈控制______________|
看着简单,但每个环节都有门道:
先别急着搞动画。我之前就吃过这亏——上来就写运动逻辑,结果画面坐标都没对齐,调了两小时还是歪的。
pythonimport tkinter as tk
from tkinter import ttk
import math
import random
class IndustrialProcess:
def __init__(self, root):
self.root = root
self.root.title("工业流程模拟系统 | 演示版 v1.0")
self.root.geometry("1000x700")
self.root.config(bg="#2C3E50")
# 创建画布 - 这是所有图形的舞台
self.canvas = tk.Canvas(root, width=1000, height=600,
bg="#34495E", highlightthickness=0)
self.canvas.pack(pady=20)
# 状态变量(核心数据层)
self.tank_level = 80.0 # 水箱液位(百分比)
self.pump_running = False # 泵运行状态
self.valve_open = False # 阀门状态
self.flow_rate = 0.0 # 当前流量
self.pump_angle = 0 # 泵叶片旋转角度
self.draw_static_elements() # 先画不动的部分
self.create_controls() # 再做控制按钮
这里藏着个小技巧:我把所有状态都集中存在类属性里,而不是散落在各个函数的局部变量。为啥?因为后面做动画更新时,你得频繁访问这些数据。集中管理,改起来不头疼。
真实的液位变化不是线性的——想象你往杯子里倒水,刚开始涨得快,越往上越慢(因为横截面积变大了)。
pythondef draw_static_elements(self):
"""绘制静态组件"""
# 【水箱】- 用多边形模拟真实罐体
tank_x, tank_y = 100, 150
tank_width, tank_height = 120, 300
# 外壳(渐变效果得手动做)
self.canvas.create_rectangle(
tank_x, tank_y, tank_x + tank_width, tank_y + tank_height,
outline="#ECF0F1", width=3, fill=""
)
# 液体区域 - 这个会动态更新
self.liquid_rect = self.canvas.create_rectangle(
tank_x + 5, tank_y + tank_height - 5, # 底部坐标固定
tank_x + tank_width - 5, tank_y + tank_height - 5,
fill="#3498DB", outline=""
)
# 液位刻度线(每20%一条)
for i in range(0, 101, 20):
y_pos = tank_y + tank_height - (tank_height * i / 100)
self.canvas.create_line(
tank_x - 10, y_pos, tank_x, y_pos,
fill="#BDC3C7", width=2
)
self.canvas.create_text(
tank_x - 25, y_pos, text=f"{i}%",
fill="#ECF0F1", font=("Arial", 10)
)
# 管道连接(简单直线就够用)
self.canvas.create_line(
tank_x + tank_width, tank_y + tank_height - 40,
300, tank_y + tank_height - 40,
fill="#95A5A6", width=8
)
注意这个细节:液体矩形的底部坐标是固定的,变化的只有顶部Y坐标。这样液位上升时,视觉效果才符合物理直觉。我第一次写的时候,傻乎乎地同时改上下坐标,结果液体像个电梯,整体上下飘……
这是整个项目里最带感的部分。要实现流畅的旋转效果,关键在于极坐标变换 + 定时器驱动。
pythondef draw_pump(self):
"""绘制泵体及叶片"""
pump_x, pump_y = 350, 350
pump_radius = 40
# 泵外壳(圆形)
self.canvas.create_oval(
pump_x - pump_radius, pump_y - pump_radius,
pump_x + pump_radius, pump_y + pump_radius,
fill="#E74C3C", outline="#C0392B", width=3
)
# 叶片(三片,每片间隔120度)
self.pump_blades = []
for i in range(3):
blade = self.canvas.create_line(
pump_x, pump_y,
pump_x + pump_radius * 0.7, pump_y,
fill="#ECF0F1", width=5, capstyle=tk.ROUND
)
self.pump_blades.append(blade)
# 中心轴
self.canvas.create_oval(
pump_x - 8, pump_y - 8, pump_x + 8, pump_y + 8,
fill="#2C3E50", outline=""
)
def rotate_pump(self):
"""泵叶片旋转逻辑"""
if not self.pump_running:
return
pump_x, pump_y = 350, 350
pump_radius = 40
# 角度递增(速度由流量决定)
self.pump_angle += 10 * (self.flow_rate / 50) # 归一化转速
for i, blade in enumerate(self.pump_blades):
# 计算每片叶片的当前角度
angle = math.radians(self.pump_angle + i * 120)
# 极坐标转直角坐标
end_x = pump_x + pump_radius * 0.7 * math.cos(angle)
end_y = pump_y + pump_radius * 0.7 * math.sin(angle)
# 更新叶片位置
self.canvas.coords(blade, pump_x, pump_y, end_x, end_y)
# 递归调用(20ms刷新一次 = 50FPS)
self.root.after(20, self.rotate_pump)
这里有个坑:after方法的时间单位是毫秒。我一开始写成20秒,结果泵转一下要等半天,还以为代码哪里死循环了……
性能优化点:如果叶片更多(比如涡轮风扇),不要每次都重新计算所有三角函数。可以预先生成角度表,用查表法替代实时计算,帧率能提升30%。
阀门的视觉设计要让人一眼看出开关状态。我用了颜色+形状的双重提示。
pythondef draw_valve(self):
"""绘制阀门"""
valve_x, valve_y = 550, 310
# 管道
self.canvas.create_line(
valve_x - 50, valve_y, valve_x + 50, valve_y,
fill="#95A5A6", width=8
)
# 阀门本体(菱形)
self.valve_body = self.canvas.create_polygon(
valve_x, valve_y - 30,
valve_x + 25, valve_y,
valve_x, valve_y + 30,
valve_x - 25, valve_y,
fill="#7F8C8D", outline="#2C3E50", width=2
)
# 状态指示器
self.valve_indicator = self.canvas.create_text(
valve_x, valve_y - 50, text="● 关闭",
fill="#E74C3C", font=("微软雅黑", 12, "bold")
)
# 绑定点击事件
self.canvas.tag_bind(self.valve_body, "<Button-1>", self.toggle_valve)
def toggle_valve(self, event=None):
"""切换阀门状态"""
self.valve_open = not self.valve_open
if self.valve_open:
# 开启态:绿色+文字提示
self.canvas.itemconfig(self.valve_body, fill="#27AE60")
self.canvas.itemconfig(self.valve_indicator,
text="● 开启", fill="#2ECC71")
if self.pump_running:
self.flow_rate = 45.0 # 基础流量
else:
# 关闭态:灰色
self.canvas.itemconfig(self.valve_body, fill="#7F8C8D")
self.canvas.itemconfig(self.valve_indicator,
text="● 关闭", fill="#E74C3C")
self.flow_rate = 0.0
交互设计心得:不要用复杂的鼠标手势。点一下切换状态,这个逻辑三岁小孩都能懂。我之前试过做"拖拽开度调节",结果测试的时候,连我自己都操作不流畅。
真实传感器的读数永远在波动——电气噪声、流体湍流、测量精度……各种因素导致数字不可能静止。所以咱得加点随机扰动。
pythondef draw_flowmeter(self):
"""绘制流量计"""
meter_x, meter_y = 750, 310
# 仪表外框
self.canvas.create_rectangle(
meter_x - 60, meter_y - 50,
meter_x + 60, meter_y + 50,
fill="#2C3E50", outline="#ECF0F1", width=3
)
# 显示屏
self.canvas.create_rectangle(
meter_x - 50, meter_y - 30,
meter_x + 50, meter_y + 10,
fill="#1C1C1C", outline=""
)
# 数字显示
self.flow_display = self.canvas.create_text(
meter_x, meter_y - 10, text="0.00",
fill="#00FF00", font=("Consolas", 20, "bold")
)
# 单位标签
self.canvas.create_text(
meter_x, meter_y + 30, text="m³/h",
fill="#BDC3C7", font=("Arial", 10)
)
def update_flow_display(self):
"""更新流量显示"""
if self.valve_open and self.pump_running:
# 基础流量 + 随机波动(±5%)
noise = random.uniform(-2.5, 2.5)
displayed_flow = max(0, self.flow_rate + noise)
else:
displayed_flow = 0.0
# 数字跳变效果(保留2位小数)
self.canvas.itemconfig(
self.flow_display,
text=f"{displayed_flow:.2f}"
)
self.root.after(100, self.update_flow_display) # 100ms刷新
为什么刷新频率比泵动画慢? 人眼对数字变化的敏感度,远低于对运动物体的敏感度。100ms刷新一次(10Hz)就够了,更快反而会导致"数字看不清"。
最后一块拼图——让水箱液位随着流量真实变化。这里要考虑流入流出平衡。
pythondef update_tank_level(self):
"""更新水箱液位"""
# 流出速率(取决于泵和阀门)
if self.pump_running and self.valve_open:
outflow = 0.15 # 每次下降0.15%
else:
outflow = 0.0
# 流入速率(模拟补水,防止水箱干涸)
if self.tank_level < 30:
inflow = 0.25 # 低液位时自动补水
else:
inflow = 0.05 # 正常补水速率
# 液位变化
self.tank_level += (inflow - outflow)
self.tank_level = max(5, min(95, self.tank_level)) # 限幅[5%, 95%]
# 更新图形
tank_x, tank_y = 100, 150
tank_height = 300
liquid_height = tank_height * (self.tank_level / 100)
self.canvas.coords(
self.liquid_rect,
105, tank_y + tank_height - liquid_height, # 顶部Y
215, tank_y + tank_height - 5 # 底部Y
)
# 低液位报警(液位<20%时闪烁)
if self.tank_level < 20:
color = "#E74C3C" if int(self.tank_level * 10) % 2 == 0 else "#C0392B"
self.canvas.itemconfig(self.liquid_rect, fill=color)
else:
self.canvas.itemconfig(self.liquid_rect, fill="#3498DB")
self.root.after(200, self.update_tank_level) # 200ms刷新
这里有个彩蛋:低液位报警的闪烁效果。通过取液位值的余数来控制颜色切换,比用额外的标志位优雅多了。
最后,给这套系统配上操作按钮:
pythondef create_controls(self):
"""创建控制面板"""
control_frame = tk.Frame(self.root, bg="#2C3E50")
control_frame.pack()
# 泵启停按钮
self.pump_btn = tk.Button(
control_frame, text="🔄 启动水泵",
command=self.toggle_pump,
bg="#27AE60", fg="white", font=("微软雅黑", 12, "bold"),
width=12, height=2, relief=tk.RAISED, bd=3
)
self.pump_btn.pack(side=tk.LEFT, padx=10)
# 阀门控制按钮
valve_btn = tk.Button(
control_frame, text="🔧 切换阀门",
command=self.toggle_valve,
bg="#3498DB", fg="white", font=("微软雅黑", 12, "bold"),
width=12, height=2
)
valve_btn.pack(side=tk.LEFT, padx=10)
# 状态栏
self.status_label = tk.Label(
self.root, text="系统就绪 | 液位: 80% | 流量: 0 m³/h",
bg="#34495E", fg="#ECF0F1", font=("Arial", 11),
pady=10
)
self.status_label.pack()
def toggle_pump(self):
"""泵启停控制"""
self.pump_running = not self.pump_running
if self.pump_running:
self.pump_btn.config(text="⏸ 停止水泵", bg="#E74C3C")
self.rotate_pump() # 启动旋转动画
if self.valve_open:
self.flow_rate = 45.0
else:
self.pump_btn.config(text="🔄 启动水泵", bg="#27AE60")
self.flow_rate = 0.0
pythonimport tkinter as tk
import math
import random
class IndustrialProcess:
def __init__(self, root):
self.root = root
self.root.title("工业流程模拟系统 v1.0")
self.root.geometry("1000x700")
self.root.config(bg="#2C3E50")
self.canvas = tk.Canvas(root, width=1000, height=600,
bg="#34495E", highlightthickness=0)
self.canvas.pack(pady=20)
# 核心状态
self.tank_level = 80.0
self.pump_running = False
self.valve_open = False
self.flow_rate = 0.0
self.pump_angle = 0
self.draw_all_components()
self.create_controls()
self.start_animations()
def draw_all_components(self):
"""绘制所有组件"""
# 水箱
self.canvas.create_rectangle(100, 150, 220, 450,
outline="#ECF0F1", width=3)
self.liquid_rect = self.canvas.create_rectangle(
105, 445, 215, 445, fill="#3498DB", outline=""
)
for i in range(0, 101, 20):
y = 450 - 300 * i / 100
self.canvas.create_line(90, y, 100, y, fill="#BDC3C7", width=2)
self.canvas.create_text(75, y, text=f"{i}%", fill="#ECF0F1")
# 管道
self.canvas.create_line(220, 410, 310, 410, fill="#95A5A6", width=8)
self.canvas.create_line(390, 410, 500, 410, fill="#95A5A6", width=8)
self.canvas.create_line(600, 410, 700, 410, fill="#95A5A6", width=8)
# 泵
self.canvas.create_oval(310, 370, 390, 450,
fill="#E74C3C", outline="#C0392B", width=3)
self.pump_blades = []
for i in range(3):
blade = self.canvas.create_line(
350, 410, 378, 410, fill="#ECF0F1", width=5
)
self.pump_blades.append(blade)
self.canvas.create_oval(342, 402, 358, 418, fill="#2C3E50")
# 阀门
self.valve_body = self.canvas.create_polygon(
550, 380, 575, 410, 550, 440, 525, 410,
fill="#7F8C8D", outline="#2C3E50", width=2
)
self.valve_indicator = self.canvas.create_text(
550, 360, text="● 关闭", fill="#E74C3C",
font=("微软雅黑", 12, "bold")
)
self.canvas.tag_bind(self.valve_body, "<Button-1>", self.toggle_valve)
# 流量计
self.canvas.create_rectangle(700, 360, 820, 460,
fill="#2C3E50", outline="#ECF0F1", width=3)
self.canvas.create_rectangle(710, 380, 810, 420, fill="#1C1C1C")
self.flow_display = self.canvas.create_text(
760, 400, text="0.00", fill="#00FF00",
font=("Consolas", 20, "bold")
)
self.canvas.create_text(760, 440, text="m³/h",
fill="#BDC3C7", font=("Arial", 10))
def create_controls(self):
"""控制面板"""
frame = tk.Frame(self.root, bg="#2C3E50")
frame.pack()
self.pump_btn = tk.Button(
frame, text="🔄 启动水泵", command=self.toggle_pump,
bg="#27AE60", fg="white", font=("微软雅黑", 12, "bold"),
width=12, height=2
)
self.pump_btn.pack(side=tk.LEFT, padx=10)
valve_btn = tk.Button(
frame, text="🔧 切换阀门", command=self.toggle_valve,
bg="#3498DB", fg="white", font=("微软雅黑", 12, "bold"),
width=12, height=2
)
valve_btn.pack(side=tk.LEFT, padx=10)
self.status_label = tk.Label(
self.root, text="系统就绪", bg="#34495E",
fg="#ECF0F1", font=("Arial", 11), pady=10
)
self.status_label.pack()
def toggle_pump(self):
"""泵控制"""
self.pump_running = not self.pump_running
if self.pump_running:
self.pump_btn.config(text="⏸ 停止水泵", bg="#E74C3C")
if self.valve_open:
self.flow_rate = 45.0
else:
self.pump_btn.config(text="🔄 启动水泵", bg="#27AE60")
self.flow_rate = 0.0
def toggle_valve(self, event=None):
"""阀门控制"""
self.valve_open = not self.valve_open
if self.valve_open:
self.canvas.itemconfig(self.valve_body, fill="#27AE60")
self.canvas.itemconfig(self.valve_indicator,
text="● 开启", fill="#2ECC71")
if self.pump_running:
self.flow_rate = 45.0
else:
self.canvas.itemconfig(self.valve_body, fill="#7F8C8D")
self.canvas.itemconfig(self.valve_indicator,
text="● 关闭", fill="#E74C3C")
self.flow_rate = 0.0
def start_animations(self):
"""启动所有动画"""
self.rotate_pump()
self.update_tank()
self.update_flow()
def rotate_pump(self):
"""泵旋转"""
if self.pump_running:
self.pump_angle += 10
for i, blade in enumerate(self.pump_blades):
angle = math.radians(self.pump_angle + i * 120)
x = 350 + 28 * math.cos(angle)
y = 410 + 28 * math.sin(angle)
self.canvas.coords(blade, 350, 410, x, y)
self.root.after(20, self.rotate_pump)
def update_tank(self):
"""液位更新"""
if self.pump_running and self.valve_open:
self.tank_level -= 0.15
if self.tank_level < 30:
self.tank_level += 0.25
else:
self.tank_level += 0.05
self.tank_level = max(5, min(95, self.tank_level))
liquid_height = 300 * (self.tank_level / 100)
self.canvas.coords(self.liquid_rect, 105, 450 - liquid_height, 215, 445)
if self.tank_level < 20:
color = "#E74C3C" if int(self.tank_level * 10) % 2 else "#C0392B"
self.canvas.itemconfig(self.liquid_rect, fill=color)
else:
self.canvas.itemconfig(self.liquid_rect, fill="#3498DB")
self.root.after(200, self.update_tank)
def update_flow(self):
"""流量显示更新"""
if self.valve_open and self.pump_running:
noise = random.uniform(-2.5, 2.5)
display = max(0, self.flow_rate + noise)
else:
display = 0.0
self.canvas.itemconfig(self.flow_display, text=f"{display:.2f}")
status_text = (f"液位: {self.tank_level:.1f}% | "
f"泵: {'运行' if self.pump_running else '停止'} | "
f"阀门: {'开启' if self.valve_open else '关闭'} | "
f"流量: {display:.2f} m³/h")
self.status_label.config(text=status_text)
self.root.after(100, self.update_flow)
if __name__ == "__main__":
root = tk.Tk()
app = IndustrialProcess(root)
root.mainloop()

运行效果:把代码复制到.py文件,双击运行。你会看到一个深色主题的界面,水箱里的蓝色液体在缓慢变化,点击"启动水泵"后,红色泵开始旋转,再点"切换阀门",流量计的绿色数字开始跳动。
如果你打算加更多组件,会发现界面开始卡顿。原因是canvas.coords()��然轻量,但频繁调用还是有开销。
解决方案:把不变的元素画到PhotoImage上,只更新动态部分。我在另一个项目里用这招,组件数从50个增加到200个,帧率依然稳定在30FPS。
现在的流量是固定值。想更逼真?可以加个PID算法,让流量根据液位自动调节。比如液位低于50%时,自动降低泵速。
python# 简易PID伪代码
error = target_level - current_level
self.flow_rate = Kp * error + Ki * integral + Kd * derivative
这块内容比较硬核,如果感兴趣,咱们下次单开一篇讲。
客户后来提了个需求:"能不能把每小时的流量存下来?"这时候就得上数据库了。好在Python的sqlite3是内置的,几行代码就能搞定:
pythonimport sqlite3
conn = sqlite3.connect('process_data.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS flow_records (
timestamp TEXT,
flow_rate REAL,
tank_level REAL
)
''')
每隔一段时间,把数据插进去,后期导出Excel分析也方便。
Q1: 为啥不用PyQt5?它不是更强大吗?
强大归强大,但学习曲线陡峭。Tkinter的API设计很直白,半天就能上手。而且PyQt5打包后的exe至少30MB起步,Tkinter只要几兆。
Q2: 动画卡顿怎么办?
检查after的时间间隔。如果设置成1ms,反而会因为来不及执行导致队列堆积。一般20-50ms是最佳区间。
Q3: 能接入真实传感器吗?
当然!用pyserial读串口数据,或者通过Modbus协议连PLC。只需把模拟的随机数,替换成真实读数就行。
这套系统可以扩展的方向还有很多:
最近我在琢磨把这套东西移植到树莓派上,配个7寸触摸屏,直接挂在车间墙上当监控大屏。成本不到五百块,比买现成的监控设备划算太多了。
最后抛个问题:如果让你设计一个"锅炉+汽轮机+发电机"的动态系统,你会怎么处理蒸汽的流动效果?欢迎留言讨论!
标签:#Python #Tkinter #工业自动化 #可视化 #动画编程
(收藏本文,下次遇到演示需求时,直接复制代码改改参数就能用!分享给同样在工控领域摸爬滚打的朋友们吧~)
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!