Tkinter报错RuntimeError:主线程不在主循环的原因与解决方法
问题:Tkinter串口实时绘图触发
RuntimeError: main thread is not in main loop 我写了一个从串口读取数据并实时绘图的Tkinter程序,只做数据读取时正常,但加上实时绘图逻辑后,就触发RuntimeError: main thread is not in main loop错误。我知道要用after方法,但用的时候出了一堆bug。想知道这个错误的成因、解决方法,以及正确的after用法。
原代码:
from tkinter import * from random import randint import numpy as np from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk) from matplotlib.figure import Figure import time import threading import serial from tkinter.filedialog import asksaveasfile import csv from skimage.restoration import (denoise_tv_chambolle, denoise_bilateral, denoise_wavelet, estimate_sigma) continuePlotting = False def change_state(): global continuePlotting global serial_port global i global b_start global mid global label if continuePlotting == True: continuePlotting = False serial_port.close() i = 2 label.pack_forget() else: continuePlotting = True label.pack(in_=mid) t, p1 = [], [] i = 0 def take_measure(): global t global p1 global t_temp global p1_temp global i global serial_port if i == 0: serial_port =serial.Serial('COM5', 2000000) serial_port.setDTR(False) time.sleep(0.1) serial_port.setDTR(True) serial_port.flushInput() p1_temp = [] t_temp = [] i = 1 elif i == 2: serial_port =serial.Serial('COM5', 2000000) serial_port.setDTR(False) time.sleep(0.1) serial_port.setDTR(True) serial_port.flushInput() p1_temp = [] t_temp = [] i = 1 try: temp=serial_port.readline()[:-2].decode() a = temp.index(";") t_val = float(temp[:a]) p1_val = (float(temp[a+1:])-2640)*20/3520 t.append(t_val) p1.append(p1_val) t_temp.append(t_val) p1_temp.append(p1_val) except: pass def app(): global t_temp global p1_temp global i global serial_port global t global p1 global b_start global mid global label root = Tk() root.config(background='white') root.geometry("1000x700") top = Frame(root) bottom = Frame(root) mid = Frame(root) top.pack(side="top") mid.pack(side="top") bottom.pack(side="bottom", fill="both", expand=True) fig = Figure() ax = fig.add_subplot(111) ax.set_xlabel("X axis") ax.set_ylabel("Y axis") ax.grid() graph = FigureCanvasTkAgg(fig, master=root) graph.get_tk_widget().pack(in_=bottom, side="top",fill='both',expand=True) graph.draw() toolbar = NavigationToolbar2Tk(graph, root, pack_toolbar=False) toolbar.update() def plotter(): while continuePlotting: take_measure() ax.clear() ax.plot(t_temp, p1_temp) graph.draw() def gui_handler(): change_state() t=threading.Thread(target=plotter, daemon=True) t.start() def Save(): files = [('CSV', '*.csv')] file_name = asksaveasfile(filetypes = files, defaultextension = files) with open(str(file_name.name),"w", newline='') as file: Writer=csv.writer(file) Writer.writerow(["temps en ms", "pression en V"]) for elt in range(len(t)): Writer.writerow([t[elt], p1[elt]]) file.close() def Clear(): global t global p1 serial_port.close() i = 0 ax.clear() graph.draw() t = [] p1 = [] def Draw(): l_t = [t[0]] l_p1 = [p1[0]] ax.cla() ax.grid() for elt in range(1,len(t)): if t[elt] == 0: l_p = denoise_wavelet(np.array(l_p1), method="VisuShrink", mode="hard", wavelet_levels=3, wavelet='haar', rescale_sigma='True') print("max", max(l_p1)) print("min", min(l_p1)) ax.plot(l_t, l_p) l_t, l_p1 = [], [] l_t.append(t[elt]) l_p1.append(p1[elt]) l_p = denoise_wavelet(np.array(l_p1), method="VisuShrink", mode="hard", wavelet_levels=3, wavelet='haar', rescale_sigma='True') ax.plot(l_t, l_p) graph.draw() print("max", max(l_p1)) print("min", min(l_p1)) b_start = Button(root, text="Start/Stop", command=gui_handler, bg="red", fg="white") b_start.pack(in_=top, side = LEFT) button_quit = Button(master=root, text="Quit", command=root.destroy) button_save = Button(root,text="Save", command=Save) button_clear = Button(root,text="Clear graph", command=Clear) button_draw = Button(root,text="Show graphs", command=Draw) button_draw.pack(in_=top, side = LEFT) button_clear.pack(in_=top, side = LEFT) button_save.pack(in_=top, side = LEFT) button_quit.pack(in_=top, side=LEFT) label = Label(root, text = "WAIT") toolbar.pack(in_=bottom, fill=X) root.mainloop() if __name__ == '__main__': app()
报错信息:
Exception ignored in: <function Variable.__del__ at 0x000001CCB0F3E3E0> Traceback (most recent call last): File "c:\Users\cbroggi\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 410, in __del__ if self._tk.getboolean(self._tk.call("info", "exists", self._name)): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RuntimeError: main thread is not in main loop Exception ignored in: <function Image.__del__ at 0x000001CCB1098D60> Traceback (most recent call last): File "c:\Users\cbroggi\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 4083, in __del__ self.tk.call('image', 'delete', self.name) RuntimeError: main thread is not in main loop Exception ignored in: <function Variable.__del__ at 0x000001CCB0F3E3E0> Traceback (most recent call last): File "c:\Users\cbroggi\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 410, in __del__ if self._tk.getboolean(self._tk.call("info", "exists", self._name)): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RuntimeError: main thread is not in main loop Exception ignored in: <function Variable.__del__ at 0x000001CCB0F3E3E0> Traceback (most recent call last): File "c:\Users\cbroggi\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 410, in __del__ if self._tk.getboolean(self._tk.call("info", "exists", self._name)): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RuntimeError: main thread is not in main loop
错误成因
- Tkinter是单线程GUI库,所有GUI相关操作(绘图、控件更新、Canvas绘制等)必须在主线程(启动
root.mainloop()的线程)中执行,子线程不能直接操作GUI元素。 - 你的代码里,
plotter函数在子线程中直接调用ax.clear()、graph.draw()这些GUI操作,违反了Tkinter的线程安全规则,导致主线程的GUI循环和子线程操作冲突,触发错误。 - 那些被忽略的异常,是子线程结束时尝试访问已被主线程销毁的Tkinter资源导致的。
解决方法
核心思路:子线程只负责读取串口数据,主线程负责更新GUI,用线程安全的队列传递数据,通过after方法让主线程定期更新绘图:
- 用
queue.Queue在子线程和主线程之间传递读取到的串口数据,避免全局变量的线程冲突。 - 子线程仅执行串口数据读取,将数据放入队列,不碰任何GUI代码。
- 主线程通过
root.after(delay_ms, func)定期调用更新绘图的函数,从队列中取出数据并更新图表,完全符合Tkinter的线程规则。
修改后的完整代码
from tkinter import * import numpy as np from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk) from matplotlib.figure import Figure import threading import serial import time from tkinter.filedialog import asksaveasfile import csv from skimage.restoration import denoise_wavelet import queue # 全局控制开关和数据队列 continuePlotting = False data_queue = queue.Queue() serial_port = None t, p1 = [], [] t_temp, p1_temp = [], [] def change_state(): global continuePlotting global serial_port global label if continuePlotting: continuePlotting = False if serial_port and serial_port.is_open: serial_port.close() label.pack_forget() else: continuePlotting = True label.pack(in_=mid) # 启动串口读取子线程 threading.Thread(target=serial_reader, daemon=True).start() # 启动主线程绘图更新 update_plot() def serial_reader(): """子线程:仅负责读取串口数据,放入队列""" global serial_port, t, p1, t_temp, p1_temp try: serial_port = serial.Serial('COM5', 2000000) serial_port.setDTR(False) time.sleep(0.1) serial_port.setDTR(True) serial_port.flushInput() # 重置临时数据列表 t_temp.clear() p1_temp.clear() except Exception as e: print(f"串口打开失败: {e}") continuePlotting = False return while continuePlotting and serial_port.is_open: try: temp = serial_port.readline()[:-2].decode().strip() if not temp: continue a = temp.index(";") t_val = float(temp[:a]) p1_val = (float(temp[a+1:])-2640)*20/3520 # 更新全局数据和临时绘图数据 t.append(t_val) p1.append(p1_val) t_temp.append(t_val) p1_temp.append(p1_val) # 把新数据放入队列(这里也可以只传新值,按需调整) data_queue.put((t_val, p1_val)) except Exception as e: # 捕获异常避免子线程崩溃 print(f"读取数据出错: {e}") continue # 线程结束时关闭串口 if serial_port.is_open: serial_port.close() def update_plot(): """主线程:定期更新绘图,用after循环调用""" global t_temp, p1_temp if continuePlotting: # 清空画布并重新绘图 ax.clear() ax.set_xlabel("X axis") ax.set_ylabel("Y axis") ax.grid() ax.plot(t_temp, p1_temp) graph.draw() # 每隔50ms再次调用自己,实现循环更新 root.after(50, update_plot) else: # 停止时可以做一些清理 pass def Save(): global t, p1 files = [('CSV', '*.csv')] file_name = asksaveasfile(filetypes=files, defaultextension=files) if not file_name: return with open(file_name.name, "w", newline='') as file: Writer = csv.writer(file) Writer.writerow(["temps en ms", "pression en V"]) for t_val, p_val in zip(t, p1): Writer.writerow([t_val, p_val]) def Clear(): global t, p1, t_temp, p1_temp, serial_port, continuePlotting continuePlotting = False if serial_port and serial_port.is_open: serial_port.close() # 清空所有数据 t.clear() p1.clear() t_temp.clear() p1_temp.clear() # 清空画布 ax.clear() ax.grid() graph.draw() def Draw(): global t, p1 if not t or not p1: return ax.cla() ax.grid() l_t = [t[0]] l_p1 = [p1[0]] for elt in range(1, len(t)): if t[elt] == 0: l_p = denoise_wavelet(np.array(l_p1), method="VisuShrink", mode="hard", wavelet_levels=3, wavelet='haar', rescale_sigma='True') print("max", max(l_p1)) print("min", min(l_p1)) ax.plot(l_t, l_p) l_t, l_p1 = [], [] l_t.append(t[elt]) l_p1.append(p1[elt]) # 处理最后一段数据 l_p = denoise_wavelet(np.array(l_p1), method="VisuShrink", mode="hard", wavelet_levels=3, wavelet='haar', rescale_sigma='True') ax.plot(l_t, l_p) graph.draw() print("max", max(l_p1)) print("min", min(l_p1)) # 初始化GUI root = Tk() root.config(background='white') root.geometry("1000x700") top = Frame(root) bottom = Frame(root) mid = Frame(root) top.pack(side="top") mid.pack(side="top") bottom.pack(side="bottom", fill="both", expand=True) fig = Figure() ax = fig.add_subplot(111) ax.set_xlabel("X axis") ax.set_ylabel("Y axis") ax.grid() graph = FigureCanvasTkAgg(fig, master=root) graph.get_tk_widget().pack(in_=bottom, side="top", fill='both', expand=True) graph.draw() toolbar = NavigationToolbar2Tk(graph, root, pack_toolbar=False) toolbar.update() # 按钮和标签 b_start = Button(root, text="Start/Stop", command=change_state, bg="red", fg="white") b_start.pack(in_=top, side=LEFT) button_draw = Button(root, text="Show graphs", command=Draw) button_clear = Button(root, text="Clear graph", command=Clear) button_save = Button(root, text="Save", command=Save) button_quit = Button(master=root, text="Quit", command=root.destroy) button_draw.pack(in_=top, side=LEFT) button_clear.pack(in_=top, side=LEFT) button_save.pack(in_=top, side=LEFT) button_quit.pack(in_=top, side=LEFT) label = Label(root, text="WAIT") toolbar.pack(in_=bottom, fill=X) root.mainloop()
关键说明
- 线程分工明确:子线程
serial_reader只做串口读取,主线程update_plot只做GUI更新,彻底避免线程冲突。 - after方法的正确用法:
root.after(50, update_plot)让主线程每隔50ms自动调用update_plot,形成循环更新,完全在Tkinter的主循环中执行,不会触发线程错误。 - 线程安全数据传递:
相关产品推荐
相关产品推荐

