基于Python-Tkinter与Arduino的SCADA系统:解决温度采集启停按钮导致GUI卡顿的问题
基于Python-Tkinter与Arduino的SCADA系统:解决温度采集启停按钮导致GUI卡顿的问题
嗨,我来帮你解决这个Tkinter GUI卡顿的问题!核心原因是你把阻塞式的while循环直接放在了按钮的回调函数里——Tkinter的主事件循环(root.mainloop())需要持续处理用户操作和界面刷新,你的循环一直占用着主线程,导致它根本没法响应新的按钮点击。咱们一步步来修复这个问题:
问题根源拆解
- Tkinter是单线程框架,所有GUI相关的操作必须在主线程中执行
- 你在
Switch()里写的while is_on是一个无限阻塞循环,会彻底卡住主事件循环,导致界面无响应 - 虽然你导入了
threading模块,但实际上没用到它,而且直接在子线程操作GUI也会引发问题(Tkinter不支持跨线程GUI操作)
最优解决方案:用Tkinter的after()方法替代阻塞循环
after(delay_ms, callback, *args)方法可以让Tkinter在指定延迟后调用一个函数,而且不会阻塞主事件循环。我们可以用它来实现定时采集温度的逻辑,同时保留启停控制的能力。
修改后的完整代码
#####################IMPORT############################## from tkinter import * import serial import time #####################Initialisation############################## arduino = serial.Serial(port='/dev/ttyACM0', baudrate=9600, timeout=.1) incrTemps = 10000 # 改成毫秒单位,对应10秒 #####################Initialisation####### is_on = False after_id = None # 用于保存after任务的ID,方便取消 #####################interface############################## root = Tk() root.title("温度采集控制器") #####################fonctions############################## def Lecture(): # 优化:处理空数据的情况,避免索引错误 data = arduino.readline().decode('utf-8').strip() if not data: return "" return data def read_temperature(): global after_id if not is_on: return # 如果已经停止,就不再继续调度 arduino.write(bytes('51','utf-8')) time.sleep(0.5) # 给Arduino一点响应时间 Data = Lecture() # 等待正确的tds数据,同时判断是否已停止采集 while Data[0:3] != "tds" and is_on: arduino.write(bytes('51','utf-8')) time.sleep(0.5) Data = Lecture() if is_on and Data.startswith("tds"): try: temp_value = float(Data[3:8]) print(f"当前温度:{temp_value}") # 可选:在GUI上显示温度值,代替仅控制台打印 temp_label.config(text=f"当前温度:{temp_value} °C") except ValueError: print("解析温度数据失败") # 调度下一次采集 after_id = root.after(incrTemps, read_temperature) def Switch(): global is_on, after_id if is_on: # 停止采集逻辑 button.config(image=off) is_on = False # 取消未执行的after任务 if after_id is not None: root.after_cancel(after_id) after_id = None else: # 启动采集逻辑 button.config(image=on) is_on = True # 触发第一次采集 read_temperature() #####################boutons############################## # 处理图片加载失败的情况,降级为文字按钮 try: on = PhotoImage(file="switch-on.png") off = PhotoImage(file="switch-off.png") except: button = Button(root, text="启动采集", bd=0, command=Switch, font=("Arial",12)) else: button = Button(root, image=on, bd=0, command=Switch) button.pack(pady=20) # 添加温度显示标签,让数据可视化 temp_label = Label(root, text="当前温度:-- °C", font=("Arial", 14)) temp_label.pack(pady=10) root.mainloop()
关键修改点说明
替换阻塞循环为
after()调度:- 把原来的
while is_on拆成read_temperature()函数,每次执行完后用after()调度下一次采集 - 这样主事件循环始终能处理用户操作,不会出现卡顿
- 把原来的
增加任务取消逻辑:
- 用
after_id保存after()返回的任务ID,停止采集时调用after_cancel()取消未执行的任务
- 用
代码健壮性优化:
- 在
Lecture()里处理空数据的情况,避免索引错误 - 增加温度解析的异常捕获,防止数据格式错误导致程序崩溃
- 添加了GUI温度显示标签,让采集数据可视化(代替仅控制台打印)
- 在
适配Tkinter习惯:
- 把
incrTemps改成毫秒单位(因为after()的延迟参数是毫秒)
- 把
额外建议
- 避免全局变量:可以把所有逻辑封装到一个类里,用实例变量代替全局变量,代码更易维护
- 串口异常处理:添加串口连接的异常捕获,比如
try-except包裹serial.Serial(),避免Arduino未连接时程序崩溃 - 线程备选方案:如果你的采集逻辑特别耗时,可以把串口操作放到子线程里,但要注意用
root.after()把采集到的数据传递到主线程更新GUI(Tkinter不允许跨线程操作GUI控件)
备注:内容来源于stack exchange,提问作者seb66
相关产品推荐
相关产品推荐

