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

Tkinter+Matplotlib实时串口绘图GUI卡顿问题求助

问题分析与解决方案

你的拖拽窗口卡顿不是正常现象,核心问题出在代码的线程安全操作和绘图效率上,以下是具体分析和修复方案:

核心问题

  1. 子线程直接操作UI:Tkinter是单线程模型,所有UI相关操作(包括Matplotlib的canvas.draw()、Tkinter的root.after())必须在主线程执行,子线程直接调用会引发线程冲突,导致GUI卡顿、无响应甚至崩溃。
  2. 阻塞式串口读取:s.read()是阻塞调用,子线程里的while counter < msg_len循环会持续占用线程资源,加剧主线程与子线程的资源竞争。
  3. 低效绘图:每次调用canvas.draw()会重绘整个画布,开销极大,是卡顿的重要诱因。

修复方案

1. 严格分离工作线程与UI线程

  • 子线程仅负责串口数据读取和预处理,不做任何UI操作。
  • 用线程安全的queue.Queue传递处理好的数据给主线程,由主线程完成绘图更新。

2. 优化串口读取与绘图效率

  • 改用read(msg_len)一次性读取指定长度数据,避免循环多次read()的开销。
  • 用canvas.draw_idle()替代canvas.draw(),延迟重绘直到UI空闲时执行,降低资源占用。

修改后的完整代码

import tkinter as tk
from tkinter import font
import serial as sr
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import threading
import queue

# 线程安全队列:传递处理后的串口数据
data_queue = queue.Queue()
cond = False
msg_len = 20  # 请根据实际需求定义该变量,原代码未显式定义

def serial_worker():
    """子线程:负责串口数据读取和预处理"""
    global cond, s
    while True:
        if not cond:
            # 停止状态时短暂等待,减少资源消耗
            threading.Event().wait(0.1)
            continue
        try:
            s.reset_input_buffer()
            # 一次性读取指定长度数据,比循环read()更高效
            raw_data = s.read(msg_len)
            if len(raw_data) != msg_len:
                print("未读取到完整数据段")
                continue
            # 转换为hex格式列表
            msg = [byte.hex() for byte in raw_data]
            print(msg)
            
            if msg[0] == '07' and msg[1] == 'e0':
                # 过滤无效数据
                filtered_msg = [x for x in msg if x not in ['07', 'e0', 'ff', 'a3', 'a7', 'c6', 'd4']]
                # 拼接为16位数据
                concat_msg = [''.join(pair) for pair in zip(filtered_msg[0::2], filtered_msg[1::2])]
                # 转换为整数
                concat_msg = [int(x, 16) for x in concat_msg]
                # 将处理好的数据放入队列
                data_queue.put(concat_msg)
            else:
                print("数据头校验失败")
                s.reset_input_buffer()
        except Exception as e:
            print(f"串口读取错误: {str(e)}")
            break

def update_plot():
    """主线程:从队列取数据并更新绘图"""
    global lines, data1
    try:
        # 一次性取出队列中所有待处理数据
        while not data_queue.empty():
            l1 = data_queue.get()
            len_each = len(l1)
            # 更新数据数组
            data1 = np.append(data1, l1[:len_each])
            # 更新曲线数据
            lines.set_xdata(np.arange(0, len(data1)))
            lines.set_ydata(data1)
            # 自动调整坐标轴范围
            ax.relim()
            ax.autoscale_view()
            # 延迟重绘,仅在UI空闲时执行
            canvas.draw_idle()
            # 清空数据,准备下一轮接收
            data1 = np.array([])
    except Exception as e:
        print(f"绘图更新错误: {str(e)}")
    # 定时调用,持续更新绘图
    root.after(50, update_plot)

# 按钮控制函数
def plot_start():
    global cond
    s.write(b"PORT IS OPEN")
    cond = True
    s.reset_input_buffer()

def plot_stop():
    global cond
    cond = False

# GUI初始化
root = tk.Tk()
root.title("Real time wala plot")
root.configure(background='light blue')
root.geometry("1920x1080")

fig = Figure()
ax = fig.add_subplot(211)
ax.set_title("FFT UP")
ax.set_xlabel("Sample")
ax.set_ylabel("values")
ax.set_xlim(0, ((msg_len-5)/2))
ax.set_ylim(0, 70000)
ax.grid()
lines = ax.plot([], [])[0]

canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().place(x=0, y=0, width=1920, height=1080)
canvas.draw()

# 网格布局配置
tk.Grid.rowconfigure(root, 2, weight=1)
tk.Grid.columnconfigure(root, 1, weight=1)

myFont = font.Font(size=12, weight='bold')

start = tk.Button(root, text="S\nT\nA\nR\nT", height=1, width=5, fg='white', bg='Green', command=plot_start)
start['font'] = myFont
stop = tk.Button(root, text="S\nT\nO\nP", height=1, width=5, fg='yellow', bg='RED', command=plot_stop)
stop['font'] = myFont

# 按钮布局
start.grid(row=0, column=0, sticky="nsew")
stop.grid(row=1, column=0, sticky="nsew")

# 串口初始化
s = sr.Serial('COM15', 115200, timeout=1)  # 添加超时,避免无限阻塞
s.reset_input_buffer()
print(s)

# 初始化数据数组
data1 = np.array([])

# 启动子线程和主线程绘图更新
threading.Thread(target=serial_worker, daemon=True).start()
root.after(100, update_plot)

root.mainloop()

# 程序退出时关闭串口
s.close()

额外优化建议

  • 限制数据长度:如果只需要显示最近的N个点,可以修改data1 = np.append(data1[-N:], l1[:len_each]),避免数据过多导致绘图卡顿。
  • 调整更新频率:如果拖拽窗口仍有轻微卡顿,可以将root.after(50, update_plot)的间隔调大到100ms,降低绘图更新频率。

内容的提问来源于stack exchange,提问作者risav rana

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 20:44:54