如何使Tkinter GUI线程安全?

3

我写了一段代码,其中有一个简单的GUI和一个画布。在这个画布上,我绘制了一个Matplot。每秒钟,Matplot都会更新一次,使用来自SQ Lite数据库的数据填充,这些数据是一些虚假的传感器信息(目前仅用于测试)。

我的问题是,画布的重新绘制导致我的窗口/GUI每秒钟都会出现卡顿。我甚至尝试在另一个线程中更新图表。但即使在那里,我也会遇到卡顿。

通过我的最新代码,我已经解决了大部分问题。多线程有助于防止我的GUI/窗口在更新画布时冻结。

我最后缺少的是使其线程安全。

这是我收到的消息:

RuntimeError: main thread is not in main loop

这是我最新使用线程的工作代码:

from tkinter import *
import random
from random import randint 
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import time
import threading
from datetime import datetime

continuePlotting = False

def change_state():
    global continuePlotting
    if continuePlotting == True:
        continuePlotting = False
    else:
        continuePlotting = True    

def data_points():
    yList = []
    for x in range (0, 20):
        yList.append(random.randint(0, 100))

    return yList

def app():
    # initialise a window and creating the GUI
    root = Tk()
    root.config(background='white')
    root.geometry("1000x700")

    lab = Label(root, text="Live Plotting", bg = 'white').pack()

    fig = Figure()

    ax = fig.add_subplot(111)
    ax.set_ylim(0,100)
    ax.set_xlim(1,30)
    ax.grid()

    graph = FigureCanvasTkAgg(fig, master=root)
    graph.get_tk_widget().pack(side="top",fill='both',expand=True)

    # Updated the Canvas 
    def plotter():
        while continuePlotting:
            ax.cla()
            ax.grid()
            ax.set_ylim(0,100)
            ax.set_xlim(1,20)

            dpts = data_points()
            ax.plot(range(20), dpts, marker='o', color='orange')
            graph.draw()
            time.sleep(1)

    def gui_handler():
        change_state()
        threading.Thread(target=plotter).start()

    b = Button(root, text="Start/Stop", command=gui_handler, bg="red", fg="white")
    b.pack()

    root.mainloop()

if __name__ == '__main__':
    app()

这里是没有线程的想法:


from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import tkinter as tk
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import sqlite3
from datetime import datetime
from random import randint

class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.parent = parent

        root.update_idletasks()

        f = Figure(figsize=(5,5), dpi=100)        
        x=1
        ax = f.add_subplot(111)        
        line = ax.plot(x, np.sin(x))        

        def animate(i):
            # Open Database
            conn = sqlite3.connect('Sensor_Data.db')
            c = conn.cursor()
            # Create some fake Sensor Data    
            NowIs = datetime.now()
            Temperature = randint(0, 100)
            Humidity = randint(0, 100)
            # Add Data to the Database
            c = conn.cursor()
            # Insert a row of data
            c.execute("insert into Sensor_Stream_1 (Date, Temperature, Humidity) values (?, ?, ?)",
                        (NowIs, Temperature, Humidity))
            # Save (commit) the changes
            conn.commit()
            # Select Data from the Database
            c.execute("SELECT Temperature FROM Sensor_Stream_1 LIMIT 10 OFFSET (SELECT COUNT(*) FROM Sensor_Stream_1)-10") 
            # Gives a list of all temperature values 
            x = 1
            Temperatures = []

            for record in c.fetchall():    
                Temperatures.append(str(x)+','+str(record[0]))
                x+=1
            # Setting up the Plot with X and Y Values
            xList = []
            yList = []

            for eachLine in Temperatures:
                if len(eachLine) > 1:
                    x, y = eachLine.split(',')
                    xList.append(int(x))
                    yList.append(int(y))

            ax.clear()

            ax.plot(xList, yList) 

            ax.set_ylim(0,100)
            ax.set_xlim(1,10)
            ax.grid(b=None, which='major', axis='both', **kwargs)


        label = tk.Label(root,text="Temperature / Humidity").pack(side="top", fill="both", expand=True)

        canvas = FigureCanvasTkAgg(f, master=root)
        canvas.get_tk_widget().pack(side="left", fill="both", expand=True)

        root.ani = animation.FuncAnimation(f, animate, interval=1000)            

if __name__ == "__main__":
    root = tk.Tk()
    MainApplication(root).pack(side="top", fill="both", expand=True)
    root.mainloop()

这是我的数据库架构:

CREATE TABLE `Sensor_Stream_1` (
    `Date`  TEXT,
    `Temperature`   INTEGER,
    `Humidity`  INTEGER
);

我会建立一个单独的类来处理数据绘制,然后在其自己的线程中将画布传递给该类。这应该可以防止您看到的延迟。话虽如此,我之前使用过matplotlib,并没有遇到延迟问题,因此可能是您代码中某些原因导致了延迟。 - Mike - SMT
这里在Stack Overflow上有许多线程示例。不过我现在正在查看你的代码,看看是否有任何可能导致这种延迟的问题。 - Mike - SMT
好的。谢谢您查看我的代码。 - K-Doe
@Mike-SMT,我修改了我的代码。尝试使用多线程,但似乎不太有效。 - K-Doe
我在想你的延迟是否来自于数据库查询。也许可以在线程中运行数据库查询。我不认为延迟来自于绘图。 - Mike - SMT
显示剩余16条评论
3个回答

5

你的GUI进程不能在任何线程中运行,只有数据采集必须是多线程的。

需要时,获取到的数据会被传输到GUI进程(或者通知GUI进程新数据可用)。在复制时,我可能需要使用互斥锁来共享采集线程和GUI之间的数据资源。

主循环将如下所示:

running = True
while running:
    root.update()
    if data_available:
        copydata_to_gui()
root.quit()

2
我遇到了与tkinter相同的问题,使用pypubsub事件是我的解决方案。 正如上面的评论所建议的那样,您必须在另一个线程中运行计算,然后将其发送到GUI线程。
import time
import tkinter as tk
import threading
from pubsub import pub

lock = threading.Lock()


class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.parent = parent
        self.label = tk.Label(root, text="Temperature / Humidity")
        self.label.pack(side="top", fill="both", expand=True)

    def listener(self, plot_data):
        with lock:
            """do your plot drawing things here"""
            self.label.configure(text=plot_data)


class WorkerThread(threading.Thread):
    def __init__(self):
        super(WorkerThread, self).__init__()
        self.daemon = True  # do not keep thread after app exit
        self._stop = False

    def run(self):
        """calculate your plot data here"""    
        for i in range(100):
            if self._stop:
                break
            time.sleep(1)
            pub.sendMessage('listener', text=str(i))


if __name__ == "__main__":
    root = tk.Tk()
    root.wm_geometry("320x240+100+100")

    main = MainApplication(root)
    main.pack(side="top", fill="both", expand=True)

    pub.subscribe(main.listener, 'listener')

    wt = WorkerThread()
    wt.start()

    root.mainloop()

0

该函数每秒钟被调用一次,它在正常刷新之外。

def start(self,parent):
    self.close=False
    self.Refresh(parent)

def Refresh(self,parent):
    '''your code'''
    if(self.close == False):
        frame.after( UpdateDelay*1000, self.Refresh, parent)

该函数被单独调用,其内部发生的所有操作都不会阻塞界面的正常运行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接