Как сделать поток Tkinter GUI безопасным? - PullRequest
0 голосов
/ 17 января 2019

Я написал фрагмент кода, в котором у меня есть простой графический интерфейс с холстом. На этом холсте я рисую Matplot. Matplot обновляется каждую секунду данными из SQ Lite DB, которые я заполняю некоторой поддельной информацией о датчике (только для тестирования в данный момент).

Моя проблема заключалась в том, что перерисовка холста заставляет мое окно / графический интерфейс зависать каждую секунду. Я даже пытался обновить сюжет в другой теме. Но даже там я получаю отставание.

С моим новейшим Кодексом я получил большинство своих вещей. Многопоточность помогает предотвратить зависание моего GUI / окна во время обновления Canvas.

Последнее, чего мне не хватает, это сделать его безопасным для потоков.

Это сообщение, которое я получаю:

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

Ответы [ 3 ]

0 голосов
/ 25 января 2019

Эта функция вызывается каждую секунду и выходит за пределы обычного обновления.

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)

Функция вызывается одна, и все, что происходит внутри нее, не блокирует нормальную работу интерфейса.

0 голосов
/ 26 января 2019

У меня была такая же проблема с tkinter, и использование pypubsub событий было моим решением. Как указано в комментариях выше, вы должны запустить свой расчет в другой ветке, а затем отправить его в ветку графического интерфейса.

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 голосов
/ 22 января 2019

Ваш процесс GUI не должен выполняться ни в одном потоке. только данные должны быть пронизаны.

При необходимости полученные данные передаются процессу графического интерфейса пользователя (или процессу графического интерфейса, получающему уведомление о наличии новых данных). Мне может понадобиться использовать мьютекс для совместного использования ресурса данных между потоком получения и графическим интерфейсом (при копировании)

основной цикл будет выглядеть так:

running = True
while running:
    root.update()
    if data_available:
        copydata_to_gui()
root.quit()
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...