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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 05:07:05