Marc
Marc

Reputation: 3313

Tkinter window not playing well with threads

I've got a program that will eventually receive data from an external source over serial, but I'm trying to develop the display-side first.

I've got this "main" module that has the simulated data send and receive. It updates a global that is used by a Matplotlib stripchart. All of this works.

#-------------------------------------------------------------------------------
# Name:        BBQData
# Purpose:  Gets the data from the Arduino, and runs the threads.
#-------------------------------------------------------------------------------
import time
import math
import random
from threading import Thread
import my_globals as bbq
import sys
import BBQStripChart as sc
import serial
import BBQControl as control


ser = serial.serial_for_url('loop://', timeout=10)

def simData():
    newTime = time.time()
    if not hasattr(simData, "lastUpdate"):
        simData.lastUpdate = newTime  # it doesn't exist yet, so initialize it
        simData.firstTime = newTime  # it doesn't exist yet, so initialize it
    if newTime > simData.lastUpdate:
        simData.lastUpdate = newTime
        return (140 + 0.05*(simData.lastUpdate - simData.firstTime), \
            145 + 0.022*(simData.lastUpdate - simData.firstTime), \
            210 + random.randrange(-10, 10))
    else:
        return None

def serialDataPump():
    testCtr = 0;
    while not bbq.closing and testCtr<100:
        newData = simData()
        if  newData != None:
            reportStr = "D " + "".join(['{:3.0f} ' for x in newData]) + '\n'
            reportStr = reportStr.format(*newData)
            ser.write(bytes(reportStr, 'ascii'))
            testCtr+=1
            time.sleep(1)
    bbq.closing = True

def serialDataRcv():
    while not bbq.closing:
        line = ser.readline()
        rcvdTime = time.time()
        temps = str(line, 'ascii').split(" ")
        temps = temps[1:-1]
        for j, x in enumerate(temps):
            bbq.temps[j].append(float(x))
            bbq.plotTimes.append(rcvdTime)

def main():
    sendThread = Thread(target = serialDataPump)
    receiveThread = Thread(target = serialDataRcv)
    sendThread.start()
    receiveThread.start()
#    sc.runUI() 
    control.runControl() #blocks until user closes window
    bbq.closing = True
    time.sleep(2)
    exit()


if __name__ == '__main__':
    main()
##    testSerMain()

However, I'd like to add a SEPARATE tkinter window that just has the most recent data on it, a close button, etc. I can get that window to come up, and show data initially, but none of the other threads run. (and nothing works when I try to run the window and the plot at the same time.)

#-------------------------------------------------------------------------------
# Name:        BBQ Display/Control
# Purpose:      displays current temp data, and control options
#-------------------------------------------------------------------------------

import tkinter as tk
import tkinter.font
import my_globals as bbq
import threading

fontSize = 78

class BBQControl(tk.Tk):
    def __init__(self,parent):
        tk.Tk.__init__(self,parent)
        self.parent = parent
        self.labelFont = tkinter.font.Font(family='Helvetica', size=int(fontSize*0.8))
        self.dataFont = tkinter.font.Font(family='Helvetica', size=fontSize, weight = 'bold')
        self.makeWindow()


    def makeWindow(self):
        self.grid()

        btnClose = tk.Button(self,text=u"Close")
        btnClose.grid(column=1,row=5)

        lblFood = tk.Label(self,anchor=tk.CENTER, text="Food Temps", \
            font = self.labelFont)
        lblFood.grid(column=0,row=0)
        lblPit = tk.Label(self,anchor=tk.CENTER, text="Pit Temps", \
            font = self.labelFont)
        lblPit.grid(column=1,row=0)

        self.food1Temp = tk.StringVar()
        lblFoodTemp1 = tk.Label(self,anchor=tk.E, \
            textvariable=self.food1Temp, font = self.dataFont)
        lblFoodTemp1.grid(column=0,row=1)

        #spawn thread to update temps
        updateThread = threading.Thread(target = self.updateLoop)
        updateThread.start()

    def updateLoop(self):
        self.food1Temp.set(str(bbq.temps[1][-1]))

def runControl():
    app = BBQControl(None)
    app.title('BBQ Display')
    app.after(0, app.updateLoop)
    app.mainloop()
    bbq.closing = True

if __name__ == '__main__':
    runControl()

Upvotes: 1

Views: 799

Answers (2)

Noctis Skytower
Noctis Skytower

Reputation: 21991

One way of getting tkinter to play well with threads is to modify the library so all method calls run on a single thread. Two other questions deal with this same problem: Updating a TKinter GUI from a multiprocessing calculation and Python GUI is not responding while thread is executing. In turn, the given answers point to several modules that help to solve the problem you are facing. Whenever I work with tkinter, I always use the safetkinter module in case threads appear to be helpful in the program.

Upvotes: 1

Bryan Oakley
Bryan Oakley

Reputation: 385800

Your title sums up the problem nicely: Tkinter doesn't play well with threads. That's not a question, that's the answer.

You can only access tkinter widgets from the same thread that created the widgets. If you want to use threads, you'll need your non-gui threads to put data on a queue and have the gui thread poll the queue periodically.

Upvotes: 1

Related Questions