Python TKinter GUI 从多个线程更新——它是线程安全的吗?一些示例代码

the*_*010 5 python multithreading tkinter matplotlib thread-safety

我一直在研究 TKinter 遇到的这一问题,即根据其他线程的数据更新 GUI。正如许多人在网上建议的那样,我诉诸于使用队列轮询策略和self.root.after()方法。

这工作得很好,但我有一个问题,我需要更新嵌入在 TKinter GUI 中的 matplotlib 图,这样做会“停用”/将焦点从 GUI 的其他方面移开/可能会阻止GUI 的其他方面。具体来说,如果我在 TKinter 条目中输入内容并且 matplotlib 图更新,则光标不再位于条目中,并且我的输入过程已被中断。

为了解决这个问题,我想我应该尝试在一个单独的线程中更新 TKinter。现在,我在网上看到很多人声称TKinter不是线程安全的,但我也看到其他人说它线程安全的。我继续制作了一个示例程序,它似乎运行得很好:即使绘图更新,光标也可以保留在条目中。但我想知道的是,这个程序安全吗?或者它是否容易出现随机或可预测的故障?我该怎么做才能拥有线程安全的 GUI?

这是我的示例代码:

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()
Run Code Online (Sandbox Code Playgroud)