I am using TKinter to build a GUI (for a socket connection to a multichannel analyzer) to receive & plot data (~15.000.000 values) in regular intervals (~15 seconds).
While receiving data I don't want the GUI to freeze, so I am using multi-threading for connection handling, data receiving & plotting operations. I accomplished this, as seen in the reproducable code, with setting an event with threading.Event()
and handle one thread after another (few lines of code in initSettings()
& acquireAndPlotData
). The only time I interfere with the GUI is when plotting to the canvas & I do this with tkinters after()
method.
When started, the code runs without freezing & receives and plots as long as the window is opened & works as expected.
As I read on handling blocking I/O operations in tkinter GUIs, I only found examples with queuing and checking the queue recursively (with Queue
& after()
,
1
2
3
4
5
), but I found it to be way more convenient and easier to handle these operations with threading.Event()
.
Now my question is:
Am I using the right approach or am I missing something important here? (regarding thread safety, race conditions, what if plotting fails and takes longer than data acquisiton? Something I don't think of? Bad practices? etc...)
I would be really thankful for feedback on this matter!
Reproducable code
#####################*** 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()
A code snipped example handled with multiple classes looks like this (same as code above, but not reproducible, can be neglected):
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)