4

I've been doing some research on this one issue I've been having with TKinter, which is updating the GUI based on data from other threads. As many have suggested online, I resorted to using the queue polling strategy with the self.root.after() method.

This works pretty well, but I have a problem in which I need to update matplotlib plots embedded in a TKinter GUI, and doing so "deactivates" / draws focus away from / perhaps blocks the other aspects of the GUI. Specifically, if I'm typing something in a TKinter entry and the matplotlib figure updates, the cursor is no longer in the entry and my typing process has been interrupted.

To solve this issue, I figured I'd try to update TKinter in a separate thread altogether. Now, I have seen online many people who claim that TKinter is not thread-safe, but I have also seen others who say it is thread-safe. I went ahead and made an example program, which seems to run pretty well: the cursor can remain in the entry even when the plot updates. But what I'd like to know is, is this program safe? Or is it susceptible to failures, random or predictable? What can I do to have a thread-safe GUI?

Here's my example code:

import Tkinter as tk
import threading
import Queue
import datetime
import math
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import matplotlib.gridspec as gridspec

gui_queue = Queue.Queue()

GUI_THEME_COLOR = 'lightblue'

##################################################
class MainGUI:
    def __init__(self, root):
        self.root = root
        self.root.title('TKinter GUI with Threaded Updates')
        self.root.minsize(width=800, height=300)
        self.root.config(background=GUI_THEME_COLOR)

        self.big_frame = tk.Frame(master=root)
        self.big_frame.pack(fill=tk.BOTH, expand=tk.YES, padx=10, pady=10)

        self.button1 = tk.Button(master=self.big_frame, text='Button 1', command=self.button1_command)
        self.button1.grid(row=0, column=0)

        self.entry1 = tk.Entry(master=self.big_frame)
        self.entry1.bind('<Return>', self.entry1_event)
        self.entry1.grid(row=1, column=0)

    def entry1_event(self, event):
        self.button1.config(text=str(self.entry1.get()))
        self.entry1.delete(0, tk.END)

    def button1_command(self):
        print 'Button 1 clicked'


class GUIManipulator(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.app = None
        self.start_time = datetime.datetime.utcnow()
        self.sec_checker = 1
        self.num_loops = 0
        self.x_list = []
        self.y_list = []

    def run(self):
        print 'Starting GUIManipulator thread...'
        while gui_queue.empty():    # Wait here until we receive the MainGUI instance
            pass
        self.app = gui_queue.get()
        while True:
            diff_time = (datetime.datetime.utcnow() - self.start_time).total_seconds()
            floor_diff_time = math.floor(diff_time)
            if floor_diff_time >= self.sec_checker:     # Configure button1 text every second
                self.app.button1.config(text=str(floor_diff_time))
                self.sec_checker += 1
                self.plot_figure()

    def plot_figure(self):
        self.num_loops += 1
        self.x_list.append(self.num_loops)
        self.y_list.append(math.sin(self.num_loops/5.0))

        if self.num_loops == 1:
            self.fig1 = Figure(figsize=(12, 6), facecolor=GUI_THEME_COLOR)
            self.fig1.suptitle('Figure 1',
                                fontsize=14,
                                fontweight='bold')

            self.gs = gridspec.GridSpec(2, 2)

            self.plot1 = self.fig1.add_subplot(self.gs[:, 0])
            self.plot1.set_title('Plot 1')
            self.plot1.set_xlabel('x')
            self.plot1.set_ylabel('sin(x)', labelpad=-10)
            self.plot1.grid(True)

        if self.num_loops > 1:
            self.plot1.cla()
        self.plot1.plot(self.x_list, self.y_list)

        if self.num_loops == 1:
            self.canvas = FigureCanvasTkAgg(self.fig1, master=self.app.big_frame)
            self.canvas.draw()
            self.canvas.get_tk_widget().grid(row=2, column=0)
        else:
            self.canvas.draw()


def main():
    root = tk.Tk()
    app = MainGUI(root)

    gui_queue.put(app)

    gui_manipulator_thread = GUIManipulator()
    gui_manipulator_thread.daemon = True
    gui_manipulator_thread.start()

    root.mainloop()


if __name__ == '__main__':
    main()
Art
  • 2,836
  • 4
  • 17
  • 34
theo1010
  • 127
  • 10
  • Possible duplicate of [python tkinter with threading causing crash](https://stackoverflow.com/questions/14168346/python-tkinter-with-threading-causing-crash) – ivan_pozdeev Apr 14 '18 at 10:41

0 Answers0