3

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
);
K-Doe
  • 509
  • 8
  • 29
  • I would build a separate class that handles drawing the data and then pass the canvas to that class while running it in its own thread. This should prevent the lag you are seeing. That said I have used matplotlib before and did not have a lag issue so there may be something you have in your code that is causing the delay. – Mike - SMT Jan 17 '19 at 13:42
  • Do you have any example for this? I have never worked with threads before. – K-Doe Jan 17 '19 at 13:44
  • There are many threading examples here on stack overflow. I am looking at your code now though to see if anything stands out that may be causing this lag. – Mike - SMT Jan 17 '19 at 13:46
  • Okay.Thank you for taking a look on my Code. – K-Doe Jan 17 '19 at 13:47
  • @Mike-SMT i edited my Code. Tried to use multithreading. Doesnt seem to work well. – K-Doe Jan 17 '19 at 13:59
  • I wonder if your lag is coming from the database query. Maybe run your database query in a thread. I do not think the lag is from drawing. – Mike - SMT Jan 17 '19 at 14:11
  • Could you take a look on the Code is posted at the bottom of my post? I tried to use threading but doesnt really now where to place the Elements in the Code. – K-Doe Jan 17 '19 at 15:36
  • It is hard to test without having a database to pull from. Maybe you can add a simple dictionary with a sample data to use in the code as a mock database. – Mike - SMT Jan 17 '19 at 15:40
  • I edited the Code with the Data hardcoded. Just drawing a line now but still get the lags. I hope you could test it like this! – K-Doe Jan 17 '19 at 15:50
  • @Mike-SMT added my newest Code to the bottom of the post. Just the right Integration of animate left. – K-Doe Jan 17 '19 at 16:23
  • @Mike-SMTI added a bounty, just i case you want to take a look again :) – K-Doe Jan 19 '19 at 14:34
  • Alright, you can find it here: https://gist.github.com/KDoeTS/7534e4bf4370f5f0cfd72a98ac38973e i just added the first code, i gues my multithreading try is trash? – K-Doe Jan 20 '19 at 08:21
  • @stovfl i updated the Github Gist again to show my latest progress. I got threading to work but now i have the problem that Tkinter isnt really thread safe. I got this message after some time: RuntimeError: main thread is not in main loop – K-Doe Jan 20 '19 at 16:14
  • @BryanOakley i deleted SQLite from the code and added a List of 20 random numbers. This makes the example a lot shorter. I am not sure but i think the rest is needed to cause/show my problem. Thanks for the hint! – K-Doe Jan 20 '19 at 17:08
  • @K-Doe: instead of saying "I am not sure but...", why not actually try it out and see what happens so that you are sure? That is how you debug problems - keep narrowing the code down until you have the absolute fewest lines of code that still gives you the problem. – Bryan Oakley Jan 20 '19 at 17:36
  • @BryanOakley I am on it! But this takes some time, you may know it as you seem to be much more experienced than me. If i have no new values or not drawing anything new on my canvas there is no error as far as i have tested. So this seems to be the minimal Code i can post. – K-Doe Jan 21 '19 at 07:11
  • @BryanOakley is it even possible to udate Tkinter GUI from anywhere else as from the mainthread? I tried the above code, also with Queue, but with Queue i have to draw on the canvas from the mainthread again which causes my window to freeze (short with one plot much more with more plots to draw). With my beginner Knowledge i am slowly at an end - i dont know what to test anymore. – K-Doe Jan 21 '19 at 07:53
  • @K-Doe: that depends on how you define "possible". Yes, it's possible to have tkinter run in one thread, and have other code run in another thread. And yes, it's possible for the GUI to show changes caused by the other thread. As a general rule, you should never try to access the tkinter objects directly from more than one thread, but other threads can use a queue to send information to the tkinter thread, and the tkinter thread can read information from that queue. – Bryan Oakley Jan 21 '19 at 13:45
  • @BryanOakley but than the Canvas is updated from the Queue which is in the tkinter thread and causing the freeze again, or am i wrong? – K-Doe Jan 21 '19 at 14:07
  • I don't know anything about how matplotlib works. There's no doubt that trying to use tkinter with threads is very difficult. – Bryan Oakley Jan 21 '19 at 14:30
  • Do you know any other way (best would be beginner friendly) to show live data in an GUI ? – K-Doe Jan 21 '19 at 16:59

3 Answers3

5

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()
sancelot
  • 1,905
  • 12
  • 31
2

I had the same problem with tkinter and using pypubsub events was my solution. As comments above suggested, you have to run your calculation in another thread, then send it to the gui thread.

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()
Sinan Cetinkaya
  • 427
  • 5
  • 18
0

This function is called every second, and it is outside the normal refresh.

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)

The function is called alone, and everything that happens inside it does not block the normal operation of the interface.