我正在使用 TKinter 构建一个 GUI(用于与多通道分析仪的套接字连接),以定期(约 15 秒)接收和绘制数据(约 15.000.000 个值)。
在接收数据时,我不希望 GUI 冻结,因此我使用多线程进行连接处理、数据接收和绘图操作。正如可重现的代码中所示,我通过设置事件来完成此操作threading.Event()
并处理一个又一个线程(几行代码initSettings()
& acquireAndPlotData
)。我唯一一次干扰 GUI 是在画布上绘图时,我使用 tkinter 来完成此操作after()
method.
启动后,只要窗口打开并按预期工作,代码就会运行而不会冻结并接收和绘图。
当我阅读有关在 tkinter GUI 中处理阻塞 I/O 操作的内容时,我只找到了递归排队和检查队列的示例(使用Queue
& after()
,
1 https://medium.com/@mattia512maldini/how-to-setup-correctly-an-application-with-python-and-tkinter-107c6bc5a45
2 https://stupidpythonideas.blogspot.com/2013/10/why-your-gui-app-freezes.html
3 https://www.oreilly.com/library/view/python-cookbook/0596001673/ch09s07.html
4 https://www.thetopsites.net/article/54237067.shtml
5 https://benedictwilkinsai.github.io/post/tkinter-mp/),但我发现用它来处理这些操作更方便、更容易threading.Event()
.
现在我的问题是:
我是否使用了正确的方法或者我在这里遗漏了一些重要的东西?(关于线程安全,竞争条件,如果绘图失败并且花费的时间比数据获取的时间长怎么办?我没有想到的东西?不好的做法?等等......)
我将非常感谢您对此事的反馈!
可重现的代码
#####################*** IMPORTS ***#######################################################
import tkinter
from tkinter import ttk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import time
import threading
import numpy as np
################### *** FUNCTIONS *** #########################################################
# *** initializes two threads for initializing connection & receiving/plotting data ***
def onStartButtonClick(event):
#
init_settings_thread.start()
acquire_and_plot_data_thread.start()
#
# *** inizialize connection & set event when finished & ready for sending data ***
def initSettings():
#time.sleep() simulates the time it takes to inizialize the connection
time.sleep(2)
start_data_acquisition_event.set()
# *** waiting for event/flag from initSettings() & start data receiving/plotting loop afer event set ***
def acquireAndPlotData():
start_data_acquisition_event.wait()
while start_data_acquisition_event.is_set():
# time.sleep() simulates the time it takes the connection to fill up the buffer
time.sleep(4)
# send updateGuiFigure to tkinters event queue, so that it won't freeze
root.after(0, updateGuiFigure)
# *** set new data points on existing plot & blit GUI canvas ***
def updateGuiFigure():
# simulate data -> 15.000.000 points in real application
line.set_xdata(np.random.rand(10))
#
line.set_ydata(np.random.rand(10))
#
plotting_canvas.restore_region(background) # restore background
ax.draw_artist(line) # redraw just the line -> draw_artist updates axis
plotting_canvas.blit(ax.bbox) # fill in the axes rectangle
#
# *** update background for resize events ***
def update_background(event):
global background
background = plotting_canvas.copy_from_bbox(ax.bbox)
##########################*** MAIN ***#########################################################
# Init GUI
root = tkinter.Tk()
# Init frame & canvas
frame = ttk.Frame(root)
plotting_area = tkinter.Canvas(root, width=700, height=400)
#
frame.grid(row=0, column=1, sticky="n")
plotting_area.grid(row=0, column=0)
# Init button & bind to function onStartButtonClick
start_button = tkinter.Button(frame, text="Start")
start_button.bind("<Button-1>", onStartButtonClick)
start_button.grid(row=0, column=0)
# Init figure & axis
fig = Figure(figsize=(7, 4), dpi=100)
ax = fig.add_subplot(111)
# Connect figure to plotting_area from GUI
plotting_canvas = FigureCanvasTkAgg(fig, master=plotting_area)
# Set axis
ax.set_title('Test')
ax.grid(True)
ax.set_xlabel('x-axis')
ax.set_ylabel('y-axis')
ax.set(xlim=[0,1], ylim=[0, 1])
# Init plot
line, = ax.plot([], [])
# if animated == True: artist (= line) will only be drawn when manually called draw_artist(line)
line.set_animated(True)
# Draw plot to GUI canvas
plotting_canvas.draw()
plotting_canvas.get_tk_widget().pack(fill=tkinter.BOTH)
background = plotting_canvas.copy_from_bbox(ax.bbox) # cache background
plotting_canvas.mpl_connect('draw_event', update_background) # update background with 'draw_event'
# Init threads
start_data_acquisition_event = threading.Event()
#
init_settings_thread = threading.Thread(name='init_settings_thread', target=initSettings, daemon=True)
acquire_and_plot_data_thread = threading.Thread(name='acquire_and_plot_data_thread', target=acquireAndPlotData, daemon=True)
# Start tkinter mainloop
root.mainloop()
使用多个类处理的代码片段示例如下所示(与上面的代码相同,但不可重现,可以忽略):
def onStartButtonClick(self):
#
.
# Disable buttons and get widget values here etc.
.
#
self.start_data_acquisition_event = threading.Event()
self.init_settings_thread = threading.Thread(target=self.initSettings)
self.acquire_and_plot_data_thread = threading.Thread(target=self.acquireAndPlotData)
#
self.init_settings_thread.start()
self.acquire_and_plot_data_thread.start()
# FUNCTION END
def initSettings(self):
self.data_handler.setInitSettings(self.user_settings_dict)
self.data_handler.initDataAcquisitionObject()
self.start_data_acquisition_event.set()
def acquireAndPlotData(self):
self.start_data_acquisition_event.wait()
while self.start_data_acquisition_event.is_set():
self.data_handler.getDataFromDataAcquisitionObject()
self.master.after(0, self.data_plotter.updateGuiFigure)