I'm writing an app that generates a live histogram to be displayed in a Tkinter window. This is more or less how the app works:
- A
Histogramclass is responsible for generating the embedded histogram inside the Tk window, collecting data and update the histogram accordingly. - There is a 'Start' button that creates a thread which is responsible for
- collecting data points and putting them in a queue,
- calling an
update_histogramfunction which pulls the new data from the queue and redraws the histogram.
- Since the function in the thread runs a loop indefinitely, there's also a 'Stop' button which stops the loop by setting an
Event(). - The
stopfunction called by the button is also called when trying to close the window while the thread is running.
The issue
Even if the same stop function is called by clicking the button or upon closing the window, if I try to close the window during a run the app freezes (is_alive() returns True), but not if I first click on the 'Stop' button and then close the window (is_alive() returns False). What am I doing wrong?
MWE
import tkinter as tk
from tkinter import ttk
from threading import Thread, Event
from queue import Queue
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
class Histogram():
def __init__(self, root: tk.Tk):
self.root = root
self.buffer: list[float] = []
self.event = Event()
self.queue = Queue()
self.stopped = False
self.fig, self.ax = plt.subplots(figsize=(4, 3), dpi=64, layout='tight')
self.ax.set_xlim(0, 80)
self.ax.set_ylim(0, 30)
self.ax.set_xlabel('Time (ns)')
self.ax.set_ylabel('Counts')
self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
self.canvas.draw()
self.canvas.get_tk_widget().grid(
column=0, columnspan=2, row=1, padx=6, pady=6, sticky='nesw'
)
def start(self) -> None:
self.cleanup() # Scrape canvas & buffer if restarting
self.thread = Thread(target=self.follow)
self.thread.start()
self.stopped = True
self.root.protocol('WM_DELETE_WINDOW', self.kill)
def follow(self) -> None:
count = 1
while not self.event.is_set():
data = np.random.normal(loc=40.0, scale=10.0)
self.queue.put(data)
self.update_histogram(n=count)
count += 1
self.event.clear()
self.stopped = True
def update_histogram(self, n: int) -> None:
data = self.queue.get()
self.buffer.append(data)
if n % 5 == 0: # Update every 5 new data points
if self.ax.patches:
_ = [b.remove() for b in self.ax.patches]
counts, bins = np.histogram(self.buffer, bins=80, range=(0, 80))
self.ax.stairs(counts, bins, color='blueviolet', fill=True)
# Add 10 to y upper limit if highest bar exceeds 95% of it
y_upper_lim = self.ax.get_ylim()[1]
if np.max(counts) > y_upper_lim * 0.95:
self.ax.set_ylim(0, y_upper_lim + 10)
self.canvas.draw()
self.queue.task_done()
def cleanup(self) -> None:
if self.ax.patches:
_ = [b.remove() for b in self.ax.patches]
self.buffer = []
def stop(self) -> None:
self.event.set()
def kill(self) -> None:
self.stop()
all_clear = self.stopped
while not all_clear:
all_clear = self.stopped
print(f'{self.thread.is_alive()=}')
self.root.quit()
self.root.destroy()
def main():
padding = dict(padx=12, pady=12, ipadx=6, ipady=6)
root = tk.Tk()
root.title('Live Histogram')
hist = Histogram(root=root)
start_button = ttk.Button(root, text='START', command=hist.start)
start_button.grid(column=0, row=0, **padding, sticky='new')
stop_button = ttk.Button(root, text='STOP', command=hist.stop)
stop_button.grid(column=1, row=0, **padding, sticky='new')
root.mainloop()
if __name__ == '__main__':
main()
Note 1: The reason why I went for this fairly complicated setup is that I've learned that any other loop run in the main thread will cause the Tkinter mainloop to freeze, so that you can't interact with any widget while the loop is running.
Note 2: I'm pretty sure I'm doing exactly what the accepted answer says in this post but here it doesn't work.
This has been driving me crazy for days! Thank you in advance :)

self.root.after(0, self.update_histogram, count)from the child thread .... usingthread.joinwill be more efficient than the busy loop that you have, but that's unrelated to your issue.