Python: threading and tkinter

Question:

I’m trying to continuously update matlibplots in tkinter GUI while being able to click on buttons to pause/continue/stop updating the plots. I’ve tried using threads, but they don’t seem to be executing parallelly (e.g. data thread is being executed but the plots don’t get updated + clicking on buttons is ignored). Why doesn’t it work?

# Import Modules
import tkinter as tk
from threading import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from scipy.fft import fft
import numpy as np
import time
import random

# global variables
state = 1             # 0 starting state; 1 streaming; 2 pause; -1 end and save
x = [0]*12
y = [0]*12

# Thread buttons and plots separately
def threading():
    state = 1
    
    t_buttons = Thread(target = buttons)
    t_plots = Thread(target = plots)
    t_data = Thread(target = data)
    
    t_buttons.start()
    t_plots.start()
    t_data.start()
    
def hex_to_dec(x, y):
    for i in range(0, 12):
        for j in range(0, len(y)):
            x[i][j] = int(str(x[i][j]), 16)
            y[i][j] = int(str(y[i][j]), 16)
    
def data():
    fig1, axs1 = main_plot()
    fig2, axs2 = FFT_plot()
    # To be replaced with actual Arduino data
    while(state!=-1):
        for i in range(0, 12):
            x[i] = [j for j in range(101)]
            y[i] = [random.randint(0, 10) for j in range(-50, 51)]
        for i in range(0, 12):
            for j in range(0, len(y)):
                x[i][j] = int(str(x[i][j]), 16)
                y[i][j] = int(str(y[i][j]), 16)

# create buttons
def stream_clicked():
    state = 1
    print("clicked")
    
def pause_clicked():
    state = 2
    print("state")
    
def finish_clicked():
    state = -1
    
    
def buttons():
    continue_button = tk.Button(window, width = 30, text = "Stream data" , 
                              fg = "black", bg = '#98FB98', command = stream_clicked)
    continue_button.place(x = window.winfo_screenwidth()*0.2, y = 0)

    pause_button = tk.Button(window, width = 30, text = "Pause streaming data" , 
                             fg = "black", bg = '#FFA000', command = pause_clicked)
    pause_button.place(x = window.winfo_screenwidth()*0.4, y = 0)

    finish_button = tk.Button(window, width = 30, text = "End session and save", 
                              fg = 'black', bg = '#FF4500', command = finish_clicked())
    finish_button.place(x = window.winfo_screenwidth()*0.6, y = 0)
    
def plots():
    fig1, axs1 = main_plot()
    fig2, axs2 = FFT_plot()
    
    if state==1:
        print("update")
        for i in range(0, 12):
            axs1[i].plot(x[i], y[i], 'blue')
            axs1[i].axes.get_yaxis().set_ticks([0], labels = ["channel  " + str(i+1)])
            axs1[i].grid(True)
            axs1[i].margins(x = 0)
        
        fig1.canvas.draw()
        fig1.canvas.flush_events()
        for i in range(0, 12):
            axs1[i].clear()
        for i in range(0, 12):
            axs2.plot(x[i], fft(y[i]))
        plt.title("FFT of all 12 channels", x = 0.5, y = 1)
        
        fig2.canvas.draw()
        fig2.canvas.flush_events()
        axs2.clear()

def main_plot():
    plt.ion()
    
    fig1, axs1 = plt.subplots(12, figsize = (10, 9), sharex = True)
    fig1.subplots_adjust(hspace = 0)
    # Add fixed values for axis
    
    canvas = FigureCanvasTkAgg(fig1, master = window)  
    canvas.draw()
    canvas.get_tk_widget().pack()
    canvas.get_tk_widget().place(x = 0, y = 35)
    
    return fig1, axs1
    
def update_main_plot(fig1, axs1):
    if state==1:
        for i in range(0, 12):
            axs1[i].plot(x[i], y[i], 'blue')
            axs1[i].axes.get_yaxis().set_ticks([0], labels = ["channel  " + str(i+1)])
            axs1[i].grid(True)
            axs1[i].margins(x = 0)
        axs1[0].set_title("Plot recordings", x = 0.5, y = 1)
        
        fig1.canvas.draw()
        fig1.canvas.flush_events()
        for i in range(0, 12):
            axs1[i].clear()
    
    
def FFT_plot():
    # Plot FFT figure 
    plt.ion()
    
    fig2, axs2 = plt.subplots(1, figsize = (7, 9))
    # Add fixed values for axis
    
    canvas = FigureCanvasTkAgg(fig2, master = window)  
    canvas.draw()
    canvas.get_tk_widget().pack()
    canvas.get_tk_widget().place(x = window.winfo_screenwidth()*0.55, y = 35)
    
    return fig2, axs2


def update_FFT_plot(fig2, axs2):
    # Update FFT plot
    for i in range(0, 12):
        axs2.plot(x[i], fft(y[i]))
    plt.title("FFT", x = 0.5, y = 1)
    
    fig2.canvas.draw()
    fig2.canvas.flush_events()
    axs2.clear()

# create root window and set its properties
window = tk.Tk()
window.title("Data Displayer")
window.geometry("%dx%d" % (window.winfo_screenwidth(), window.winfo_screenheight()))
window.configure(background = 'white')

threading()

window.mainloop()

*** Sometimes it just doesn’t work without any message and sometimes I also get "RuntimeError: main thread is not in main loop" ***

Asked By: AMcoding

||

Answers:

to be fair all functions in your code are very likely to cause a segmentation fault, and other functions that don’t result in a segmentation fault simply don’t work, it’s hard to explain what’s wrong.

  1. define global variables as global if you are going to modify them
  2. update GUI in your main thread by using the window.after method repeatedly.
  3. only reading from your microcontroller should be done in separate thread.
  4. creation of Tkinter objects should be done in the main thread, only updates are allowed in other threads, but it is not thread-safe, so while it may work it can lead to some weird behavior or errors sometimes.
  5. calling matplotlib functions such as ion and flush_events causes errors because these are for matplotlib interactive canvas, not for tkinter canvas.
  6. threading has a very tough learning curve so ask yourself "do i really need threads in here" and "is there any way to not use threads" before you attempt to use them as once you start using threads you are no longer using python’s "safe code", despite all the efforts, threads are not safe to use for any task, it’s up to you to make them safe, and to be honest threads are not needed here unless you are reading 1 GB/s from your microcontroller.
  7. don’t use numbers for states, it’s not pythonic, and it confuses the readers, and it has no performance benefit over using Enums.
  8. programs are built incrementally, not copy-paste from multiple working snippets, as it is harder to track where the error comes from when multiple parts of the code weren’t verified to be working.
# Import Modules
import tkinter as tk
from threading import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from scipy.fft import fft
import numpy as np
import time
import random
from enum import Enum,auto

UPDATE_INTERVAL_MS = 300

class States(Enum):
    STREAM = auto()
    PAUSE = auto()
    SAVE = auto()
    START = auto()


# global variables
state = States.START  # check States Enum
x = [[0]]*12
y = [[0]]*12


# Thread buttons and plots separately
def threading():
    global state
    global window
    state = States.STREAM

    buttons()
    plots()
    data()
    t_grab_data = Thread(target=grab_data_loop,daemon=True)
    t_grab_data.start()
    # t_buttons = Thread(target=buttons)
    # t_plots = Thread(target=plots)
    # t_data = Thread(target=data)
    #
    # t_buttons.start()
    # t_plots.start()
    # t_data.start()


def hex_to_dec(x, y):
    for i in range(0, 12):
        for j in range(0, len(y)):
            x[i][j] = int(str(x[i][j]), 16)
            y[i][j] = int(str(y[i][j]), 16)


def data():
    global fig1,axs1,fig2,axs2
    fig1, axs1 = main_plot()
    fig2, axs2 = FFT_plot()
    # To be replaced with actual Arduino data
    window.after(UPDATE_INTERVAL_MS,draw_data_loop)


def grab_data_loop():
    while state != States.SAVE:
        for i in range(0, 12):
            x[i] = [j for j in range(101)]
            y[i] = [random.randint(0, 10) for j in range(-50, 51)]
        for i in range(0, 12):
            for j in range(0, len(y)):
                x[i][j] = int(str(x[i][j]), 16)
                y[i][j] = int(str(y[i][j]), 16)
        time.sleep(0.1)  # because we are not reading from a microcontroller

def draw_data_loop():
    if state == States.STREAM:
        update_main_plot(fig1, axs1)
        update_FFT_plot(fig2, axs2)
    window.after(UPDATE_INTERVAL_MS,draw_data_loop)


# create buttons
def stream_clicked():
    global state
    state = States.STREAM
    print("clicked")


def pause_clicked():
    global state
    state = States.PAUSE
    print("state")


def finish_clicked():
    global state
    state = States.SAVE
    window.destroy()


def buttons():
    continue_button = tk.Button(window, width=30, text="Stream data",
                                fg="black", bg='#98FB98', command=stream_clicked)
    continue_button.place(x=window.winfo_screenwidth() * 0.2, y=0)

    pause_button = tk.Button(window, width=30, text="Pause streaming data",
                             fg="black", bg='#FFA000', command=pause_clicked)
    pause_button.place(x=window.winfo_screenwidth() * 0.4, y=0)

    finish_button = tk.Button(window, width=30, text="End session and save",
                              fg='black', bg='#FF4500', command=finish_clicked)
    finish_button.place(x=window.winfo_screenwidth() * 0.6, y=0)


def plots():
    global state
    fig1, axs1 = main_plot()
    fig2, axs2 = FFT_plot()

    if state == States.STREAM:
        print("update")
        for i in range(0, 12):
            axs1[i].plot(x[i], y[i], 'blue')
            axs1[i].axes.get_yaxis().set_ticks([0], labels=["channel  " + str(i + 1)])
            axs1[i].grid(True)
            axs1[i].margins(x=0)

        # fig1.canvas.draw()
        # fig1.canvas.flush_events()
        # for i in range(0, 12):
        #     axs1[i].clear()
        for i in range(0, 12):
            axs2.plot(x[i], np.abs(fft(y[i])))
        plt.title("FFT of all 12 channels", x=0.5, y=1)

        # fig2.canvas.draw()
        # fig2.canvas.flush_events()
        # axs2.clear()


def main_plot():
    # plt.ion()
    global canvas1
    fig1, axs1 = plt.subplots(12, figsize=(10, 9), sharex=True)
    fig1.subplots_adjust(hspace=0)
    # Add fixed values for axis

    canvas1 = FigureCanvasTkAgg(fig1, master=window)
    # canvas.draw()
    canvas1.get_tk_widget().pack()
    canvas1.get_tk_widget().place(x=0, y=35)

    return fig1, axs1


def update_main_plot(fig1, axs1):
    if state == States.STREAM:
        for i in range(0, 12):
            axs1[i].clear()
        for i in range(0, 12):
            axs1[i].plot(x[i], y[i], 'blue')
            axs1[i].axes.get_yaxis().set_ticks([0], labels=["channel  " + str(i + 1)])
            axs1[i].grid(True)
            axs1[i].margins(x=0)
        axs1[0].set_title("Plot recordings", x=0.5, y=1)
        canvas1.draw()
        # fig1.canvas.draw()
        # fig1.canvas.flush_events()



def FFT_plot():
    # Plot FFT figure
    # plt.ion()
    global canvas2
    fig2, axs2 = plt.subplots(1, figsize=(7, 9))
    # Add fixed values for axis

    canvas2 = FigureCanvasTkAgg(fig2, master=window)
    # canvas.draw()
    canvas2.get_tk_widget().pack()
    canvas2.get_tk_widget().place(x=window.winfo_screenwidth() * 0.55, y=35)

    return fig2, axs2


def update_FFT_plot(fig2, axs2):
    # Update FFT plot
    if state == States.STREAM:
        axs2.clear()
        for i in range(0, 12):
            axs2.plot(x[i], np.abs(fft(y[i])))
        plt.title("FFT", x=0.5, y=1)
        canvas2.draw()
    # fig2.canvas.draw()
    # fig2.canvas.flush_events()
    # axs2.clear()


# create root window and set its properties
window = tk.Tk()
window.title("Data Displayer")
window.geometry("%dx%d" % (window.winfo_screenwidth(), window.winfo_screenheight()))
window.configure(background='white')

threading()

window.mainloop()

# put saving logic here
Answered By: Ahmed AEK
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.