You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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()

关键修改点说明

  1. 替换阻塞循环为after()调度:

    • 把原来的while is_on拆成read_temperature()函数,每次执行完后用after()调度下一次采集
    • 这样主事件循环始终能处理用户操作,不会出现卡顿
  2. 增加任务取消逻辑:

    • 用after_id保存after()返回的任务ID,停止采集时调用after_cancel()取消未执行的任务
  3. 代码健壮性优化:

    • 在Lecture()里处理空数据的情况,避免索引错误
    • 增加温度解析的异常捕获,防止数据格式错误导致程序崩溃
    • 添加了GUI温度显示标签,让采集数据可视化(代替仅控制台打印)
  4. 适配Tkinter习惯:

    • 把incrTemps改成毫秒单位(因为after()的延迟参数是毫秒)

额外建议

  • 避免全局变量:可以把所有逻辑封装到一个类里,用实例变量代替全局变量,代码更易维护
  • 串口异常处理:添加串口连接的异常捕获,比如try-except包裹serial.Serial(),避免Arduino未连接时程序崩溃
  • 线程备选方案:如果你的采集逻辑特别耗时,可以把串口操作放到子线程里,但要注意用root.after()把采集到的数据传递到主线程更新GUI(Tkinter不允许跨线程操作GUI控件)

备注:内容来源于stack exchange,提问作者seb66

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 18:22:59