单线程下实现GUI按钮多次触发串口数据读取函数的解决方案
串口读取GUI按钮重复执行问题解决
问题背景
我在GUI中设置了一个按钮用于从串口读取数据,初始代码点击一次后无法重复触发,报错thread can only be started once。需求是点击按钮即可触发数据读取,多次点击能重复执行,同时用线程避免GUI冻结。
初始代码(存在单次执行问题)
def takeRead(): ser = serial.Serial() ser.baudrate = 115200 ser.port = "COM7" # "COM#" this must match with connected port ser.open() #opening serial port while True: global data, x, y0 y0 = [] data = [] x = [] for j in range(8000): line = ser.readline() # read a byte string if line: string = line.decode() # convert the byte string to a unicode string res = [int(i) for i in string.split() if i.isdigit()] #saperating integer from string data.append(res) #append readings to store # print(res) x0= j*0.9 x.append(x0) # print(x0) if j<4000: y = (5/4000)*j else: y = (-5/4000)*j + 10 y0.append(y) data = [val for sublist in data for val in sublist] # ax.plot(x,y0,color='r') break # print(y0) ser.close() takeread = Button(graph_frame, text="Take Readings", command=threading.Thread(target=takeRead).start, font=20, width=20) takeread.pack(side=BOTTOM)
问题原因:直接把threading.Thread(target=takeRead).start绑定为按钮command,本质是每次点击调用同一个线程对象的start方法,而线程只能启动一次,第二次点击就会报错。
错误尝试的代码
def takeRead(): global takereadings def takereadings(): takereading = True while takereading == True: ser = serial.Serial() ser.baudrate = 115200 ser.port = "COM7" # "COM#" this must match with connected port ser.open() #opening serial port global data, x, y0 y0 = [] data = [] x = [] for j in range(8000): line = ser.readline() # read a byte string if line: string = line.decode() # convert the byte string to a unicode string res = [int(i) for i in string.split() if i.isdigit()] #saperating integer from string data.append(res) #append readings to store # print(res) x0= j*0.9 x.append(x0) # print(x0) if j<4000: y = (5/4000)*j else: y = (-5/4000)*j + 10 y0.append(y) data = [val for sublist in data for val in sublist] # ax.plot(x,y0,color='r') break # print(y0) ser.close() takereading = False threading.Thread(target=takeRead).start() takeread = Button(graph_frame, text="Take Readings", command=takereadings, font=20, width=20) takeread.pack()
问题原因:嵌套函数的作用域逻辑混乱,全局变量takereadings的绑定关系错误,线程内的循环控制也没有实现重复触发的效果。
正确解决方案
核心思路:每次点击按钮时创建新的线程执行读取逻辑,避免重复启动同一个线程;同时规范串口资源的释放,避免GUI冻结。
import serial import threading from tkinter import Button, Frame, BOTTOM # 假设graph_frame是已创建的GUI容器 graph_frame = Frame() def takeRead(): # 用局部变量替代全局变量,降低代码耦合 y0 = [] data = [] x = [] ser = None try: ser = serial.Serial() ser.baudrate = 115200 ser.port = "COM7" ser.open() for j in range(8000): line = ser.readline() if line: string = line.decode().strip() # 直接扩展列表,省去后续扁平化操作 res = [int(i) for i in string.split() if i.isdigit()] data.extend(res) x0 = j * 0.9 x.append(x0) if j < 4000: y = (5/4000)*j else: y = (-5/4000)*j + 10 y0.append(y) # 若需更新GUI(如绘图),必须在主线程执行,用after方法调度 # 示例:graph_frame.after(0, lambda: ax.plot(x, y0, color='r')) except Exception as e: print(f"串口读取错误: {e}") finally: # 确保串口无论是否出错都会关闭,避免资源泄漏 if ser and ser.is_open: ser.close() def on_take_read_click(): # 每次点击按钮,新建线程并启动 threading.Thread(target=takeRead, daemon=True).start() takeread = Button(graph_frame, text="Take Readings", command=on_take_read_click, font=20, width=20) takeread.pack(side=BOTTOM) # 启动GUI主循环(根据实际场景添加) # graph_frame.mainloop()
关键优化点
- 按钮绑定独立的点击处理函数,每次点击创建新线程,彻底解决线程只能启动一次的问题
- 使用
daemon=True标记线程,让线程随GUI主线程退出,避免程序残留后台线程 - 用
try-finally块确保串口资源被正确释放,防止串口占用 - 减少全局变量使用,改用局部变量提升代码可维护性
- 若需更新GUI,通过
after方法将操作调度到主线程执行,符合GUI框架的线程安全要求
内容的提问来源于stack exchange,提问作者IMI
相关产品推荐
相关产品推荐

