基于Tkinter GUI的PC与树莓派UDP通信GUI崩溃问题求助
解决Tkinter GUI与UDP交互时崩溃的问题
Tkinter采用单线程主事件循环模型,所有GUI操作必须在主线程执行。你的代码中,点击按钮后直接在主线程执行UDP收发(sock.recvfrom是阻塞操作),会卡住主事件循环,导致GUI无响应甚至崩溃。
解决方案是把阻塞的UDP通信逻辑放到子线程中执行,主线程仅处理GUI相关的更新操作。
修改后的完整代码
import socket, time import pickle from matplotlib.figure import Figure import matplotlib.pyplot as plt from matplotlib.transforms import IdentityTransform from tkinter import * import tkinter as tk from tkinter import messagebox from tkinter import ttk import csv import os import seaborn as sns from threading import Thread serverAddress = ('172.16.14.75', 2222) # 树莓派地址 LEN_UDP_BUFFER = 1024 def sock_rdt(s, timeout=1800.0): s.settimeout(timeout) try: return s.recvfrom(LEN_UDP_BUFFER) except: return "".encode('utf-8'), ("", 0) UDPClient = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def Heating_cmd(): def worker(): cmd = 'Heating'.encode('utf-8') UDPClient.sendto(cmd, serverAddress) data, address = sock_rdt(UDPClient) if data: print(pickle.loads(data)) else: # 在主线程弹出超时提示 root.after(0, lambda: messagebox.showwarning("超时", "接收超时!")) # 启动守护线程,避免程序退出时残留线程 Thread(target=worker, daemon=True).start() def Emptying_cmd(): def worker(): cmd = 'Emptying'.encode('utf-8') UDPClient.sendto(cmd, serverAddress) data, address = sock_rdt(UDPClient) if not data: root.after(0, lambda: messagebox.showwarning("超时", "接收超时!")) Thread(target=worker, daemon=True).start() def Cleaning_cmd(): def worker(): cmd = 'Cleaning'.encode('utf-8') UDPClient.sendto(cmd, serverAddress) data, address = sock_rdt(UDPClient) if data: print(pickle.loads(data)) else: root.after(0, lambda: messagebox.showwarning("超时", "接收超时!")) Thread(target=worker, daemon=True).start() def Close(): def worker(): UDPClient.sendto("stop server".encode('utf-8'), serverAddress) UDPClient.close() # 在主线程销毁GUI root.after(0, root.destroy) Thread(target=worker, daemon=True).start() def Analysis_cmd(): def worker(): cmd = 'Analysis'.encode('utf-8') UDPClient.sendto(cmd, serverAddress) data, address = sock_rdt(UDPClient) if data: output = pickle.loads(data) # 修复datapath的笔误,改为filepath with open(filepath + '/' + 'Misure.csv', "a") as f: writer = csv.writer(f) writer.writerow(output[:-1]) message1 = f"ISO Code: {output[5]}" message2 = f"RH index ([0 - 100 %]): {output[2]}" message3 = f"Contamination index ([0 - 13]): {output[1]}" message4 = f"Viscosity index: {output[4]}" message5 = f"Oil Temperature (°C): {output[3]}" # 绘制直方图 intervals = list(range(0,110,10)) fig, ax = plt.subplots(figsize=(15,8)) sns.histplot(data=output[-1], bins=intervals, stat='count') ax.set_xticks(ticks=intervals,fontsize=8) ax.set_xlabel('Particle size (μm)') ax.set_ylabel('Counts') ax.text(700, 500, message1, color="firebrick", fontsize=15,transform=IdentityTransform()) ax.text(700, 450, message2, color="dimgray", fontsize=15,transform=IdentityTransform()) ax.text(700, 400, message3, color="dimgray", fontsize=15,transform=IdentityTransform()) ax.text(700, 350, message4, color="dimgray", fontsize=15,transform=IdentityTransform()) ax.text(700, 300, message5, color="dimgray", fontsize=15,transform=IdentityTransform()) plt.show() else: root.after(0, lambda: messagebox.showwarning("超时", "接收超时!")) Thread(target=worker, daemon=True).start() def get_value(): global filename, filepath filename = file_entry.get() filepath = path_entry.get() fields = ['timestamp','Contamination_Index','RH_Index','Oil_Temperature','Viscosity_Index','ISO','>4 micro','>6 micro','>14 micro'] if not file_entry.get() and not path_entry.get(): messagebox.showerror(title="Error", message='请输入路径和文件名') elif not os.path.isdir(filepath): messagebox.showerror(title="Error", message='无效的目录路径') elif not file_entry.get(): messagebox.showerror(title="Error", message='无效的文件名') else: if not(os.path.exists(filepath + '/' + filename + '.csv')): with open(filepath + '/' + filename + '.csv', "w+") as f: writer = csv.writer(f) writer.writerow(fields) # 图形用户界面 root = Tk() root.title('Oil Analyzer') root.geometry('460x460') root.resizable(False,False) root.config(bg = 'grey85') s = ttk.Style() colorFrame = 'DarkSeaGreen1' colorButtonbase = 'SteelBlue1' colorButtonactive = 'SteelBlue3' s.configure('new.TFrame', background=colorFrame) frame1 = ttk.Frame(root,borderwidth=5, relief="ridge", style='new.TFrame') frame2 = ttk.Frame(root,borderwidth=5, relief="ridge", style='new.TFrame') login_label = Label(frame1, text = "数据保存信息", bg=colorFrame, fg='OrangeRed3', font =("Calibri",14)) path_label = Label(frame1, text = "目录路径", bg=colorFrame, font =("Calibri",12), justify="right") path_entry = Entry(frame1, width=30) file_entry = Entry(frame1, width=30) file_label = Label(frame1, text = "文件名",bg=colorFrame, font =("Calibri",12), justify="right") save_button = Button(frame1, text = "保存", bg = 'indian red', activebackground = 'IndianRed4', font =("Calibri",12), command = get_value) commands_label = Label(frame2, text = "油液分析操作", bg=colorFrame, fg='OrangeRed3', font =("Calibri",14)) button1 = Button(frame2, text='加热', command = Heating_cmd, bg = colorButtonbase, activebackground = colorButtonactive, font =("Calibri",12)) button2 = Button(frame2,text='分析', command = Analysis_cmd, bg = colorButtonbase, activebackground = colorButtonactive, font =("Calibri",12)) button3 = Button(frame2, text='排空', command = Emptying_cmd, bg = colorButtonbase, activebackground = colorButtonactive, font =("Calibri",12)) button4 = Button(frame2,text='清洗', command = Cleaning_cmd, bg = colorButtonbase, activebackground = colorButtonactive, font =("Calibri",12)) exit_button = Button(frame2, text='退出', command = Close, bg = 'indian red', activebackground = 'IndianRed4', font =("Calibri",12)) frame1.grid(column=0, row=0, padx=20, pady=20, sticky="nsew") login_label.grid(column=0, columnspan = 2, row=0, pady=10, padx=10, sticky=W) path_label.grid(column=0, row=1, padx=15, pady=5,sticky = E) path_entry.grid(column=1, row=1, padx=5, pady=5) file_label.grid(column=0, row=2, padx=15, pady=5, sticky = E) file_entry.grid(column=1, row=2, padx=5, pady=5) save_button.grid(column=4, row=2, padx=18, pady=8, ipadx=5, ipady=5) frame2.grid(column=0, row=6, padx=20, pady=20, sticky="nsew") commands_label.grid(column=1, row=5, columnspan = 2,pady=10, padx=10, sticky=W) button1.grid(column=1, row=6, padx=15, pady=15, ipady=10, ipadx=30, sticky = E) button2.grid(column=1, row=7, padx=15, pady=15, ipady=10, ipadx=30,sticky = E) button3.grid(column=2, row=6, padx=15, pady=15, ipady=10, ipadx=30,sticky = W) button4.grid(column=2, row=7, padx=15, pady=15, ipady=10, ipadx=30,sticky = W) exit_button.grid(column=4, row=7, padx=18, ipadx=8, ipady=5) root.mainloop()
关键修改说明
- 子线程执行阻塞操作:所有包含UDP收发(
sendto/recvfrom)的指令逻辑都封装到内部worker函数,通过Thread启动子线程执行,避免阻塞Tkinter主事件循环。 - 主线程更新GUI:子线程中需要更新GUI的操作(比如弹出消息框、销毁窗口),必须通过
root.after(0, callback)回调到主线程执行,因为Tkinter禁止子线程直接操作GUI组件。 - 守护线程设置:子线程设置
daemon=True,这样当主窗口关闭时,子线程会自动终止,避免残留进程。 - 修复变量笔误:将
Analysis_cmd中的datapath改为filepath,匹配get_value中定义的全局变量。 - 中文界面优化:将部分英文提示改为中文,提升使用体验。
内容的提问来源于stack exchange,提问作者Marghe_l
相关产品推荐
相关产品推荐

