Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.2k views
in Technique[技术] by (71.8m points)

python - How to make Tkinter GUI thread safe?

I have written a piece of code where I have a simple GUI with an canvas. On this canvas I draw a Matplot. The Matplot is updated every second with data from an SQ Lite DB which I fill with some fake Sensor information (just for testing at the moment).

My Problem was that the redrawing of the canvas causes my window/gui to lag every second. I even tried to update the plot in another thread. But even there I get an lag.

With my newest Code i got most of my things working. Threading helps to prevent my GUI/Window from freezing while the Canvas is updated.

The last thing I miss is to make it Thread safe.

This is the message I get:

RuntimeError: main thread is not in main loop

Here is my newest working code with threading:

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()

Here the idea without a thread:

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()

Here is my DB Schema:

CREATE TABLE `Sensor_Stream_1` (
    `Date`  TEXT,
    `Temperature`   INTEGER,
    `Humidity`  INTEGER
);
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Your GUI process must not run in any thread. only the dataacquisition must be threaded.

When needed , the data acquired are transfered to the gui process (or the gui process notified from new data available) . I may need to use a mutex to share data resource between acquisition thread and gui (when copying)

the mainloop will look like :

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...