SteveP
SteveP

Reputation: 23

Python: How to update a GUI with variables from a separate process while maintaining GUI interactivity

After quite a bit of reading here about multiple processes, pipes, etc., I haven't found an answer yet, but my apologies if it already exists.

I have a piece of peripheral hardware for which I'm trying to create a GUI. I'd like to have the GUI get updated constantly with data from the peripheral, while still maintaining interactivity for the user. For example, I have a gain parameter that I'm using to drive a bargraph, and while that is constantly being updated, I'd like the user to be able to click a button to cause some action. Here is some example code. Despite my certainty that I have some serious mistakes here, this actually almost works, but the 'quit' button remains unresponsive:

#!/usr/bin/env python`
# -*- coding: utf-8 -*-
# 2014-07-24 S. Petit

import matplotlib.pyplot as plt
from serial import Serial
import serial, socket, time, datetime, sys, struct
from datetime import datetime
import numpy as np
import shutil
import os
from random import randint
from Tkinter import *
from multiprocessing import *

dcbSerialPort = 'COM10'

def getGainLNA(pipeToParent):
    try:
        S_dcb = Serial(dcbSerialPort, 115200, timeout=.1)
        print 'Opened DCB at', dcbSerialPort
    except:
        print '\r\n'
        print '*************************************************'
        print 'ERROR: Unable to open', dcbSerialPort, 'serial connection.'
        print '*************************************************'
        print '\r\n'
        raw_input()
        exit()

    while True:
        promptFound = False
        PICreturn = ''
        S_dcb.write('gain\r')
        while not promptFound:
            PICreturn += S_dcb.read(S_dcb.inWaiting())
            if 'DCB>' in PICreturn:
                promptFound = True

        gainLNA = float(PICreturn[20:28].strip())
        gainLNA_scaled = int(100*(gainLNA/33))

        pipeToParent.send(gainLNA_scaled)

    return()

if __name__ == '__main__':

    gainUpdaterPipe, gainUpdaterPipeChild = Pipe()

    lnaGainUpdater = Process(target=getGainLNA, args=(gainUpdaterPipeChild,))
    lnaGainUpdater.start()

    root=Tk()
    root.title = 'AGC'

    while True:
        if gainUpdaterPipe.poll():
            gainLNA = gainUpdaterPipe.recv()
            print gainLNA

            quitButton = Button(text='Quit', command=quit)
            quitButton.grid(row=1, column=0)

            areaAGC = Canvas(width=120, height=100, bg='blue')
            objectAGC = areaAGC.create_polygon(20,20, gainLNA,20, gainLNA,50, 20,50, outline='green', fill='yellow')
            areaAGC.grid(row=0, column=0)

        root.update_idletasks()

Thanks for any help... Steve P

EDIT: Okay, after attempting to make use of @ebarr's example, here's what I have. The label widget updates with the count but the bargraph does not:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 2014-07-24 S. Petit

import matplotlib.pyplot as plt
from serial import Serial
import serial, socket, time, datetime, sys, struct
from datetime import datetime
import numpy as np
import shutil
import os
from random import randint
import Tkinter as tk
from multiprocessing import *

dcbSerialPort = 'COM10'

# count from 0 to infinity, writing the value to a pipe
def count(pipe,stop):
    ii = 0
    while not stop.is_set():
        ii+=1
        pipe.send(ii)
        time.sleep(1)

class UpdatingGUI(tk.Frame):
    def __init__(self,parent):
        tk.Frame.__init__(self,parent)
        self.parent = parent
        self.parent_pipe, self.child_pipe = Pipe()
        self.stop_event = Event()

        # label to show count value
        self.updating_int = tk.IntVar()
        self.updating_int.set(0)
        self.updating_lbl = tk.Label(self,textvariable=self.updating_int)
        self.updating_lbl.pack()

        # bargraph to show count value
        self.area_barGraph = tk.Canvas(width=120, height=100, bg='blue')
        self.bargraph = self.area_barGraph.create_polygon(10,10, (10+self.updating_int.get()),10, (10+self.updating_int.get()),20, 10,20, outline='green', fill='yellow')
        self.area_barGraph.pack()

        # button that will stay responsive to requests while count is on going
        self.quit_btn = tk.Button(self,text="Quit",command=self.quit)
        self.quit_btn.pack()

        # launch count as a process
        self.counter = Process(target=count,args=(self.child_pipe,self.stop_event))
        self.counter.start()

        # call an update method to check the pipe and update the label
        self.update()

    def quit(self):
        self.stop_event.set()
        self.parent.destroy()

    def update(self):
        # While the pipe has data, read and update the StringVar
        while self.parent_pipe.poll():
            self.updating_int.set(self.parent_pipe.recv())

        # set the update method to run again in 1 seconds time
        self.parent.after(1000,self.update)


def main():
    root = tk.Tk()
    gui = UpdatingGUI(root)
    gui.pack()
    root.mainloop()

# print __name__

if __name__ == "__main__":
    main()

Upvotes: 2

Views: 3138

Answers (1)

ebarr
ebarr

Reputation: 7842

You are pretty close to a working solution. As is noted in one of the comments above, using the tkinter after will solve most of your problem.

Below is a minimal example of a separate process (running a simple counter) passing a state that can be used to update your GUI:

import Tkinter as tk
from multiprocessing import Event,Process,Pipe
from time import sleep

# count from 0 to infinity, writing the value to a pipe
def count(pipe,stop):
    ii = 0
    while not stop.is_set():
        ii+=1
        pipe.send(ii)
        sleep(1)

class UpdatingGUI(tk.Frame):
    def __init__(self,parent):
        tk.Frame.__init__(self,parent)
        self.parent = parent
        self.parent_pipe, self.child_pipe = Pipe()
        self.stop_event = Event()

        # label to show count value
        self.updating_txt = tk.StringVar()
        self.updating_txt.set("Waiting...")
        self.updating_lbl = tk.Label(self,textvariable=self.updating_txt)
        self.updating_lbl.pack()

        # button that will stay responsive to requests while count is on going
        self.quit_btn = tk.Button(self,text="Quit",command=self.quit)
        self.quit_btn.pack()

        # launch count as a process
        self.counter = Process(target=count,args=(self.child_pipe,self.stop_event))
        self.counter.start()

        # call an update method to check the pipe and update the label
        self.update()

    def quit(self):
        self.stop_event.set()
        self.parent.destroy()

    def update(self):
        # While the pipe has data, read and update the StringVar
        while self.parent_pipe.poll():
            self.updating_txt.set(self.parent_pipe.recv())

        # set the update method to run again in 1 seconds time
        self.parent.after(1000,self.update)


def main():
    root = tk.Tk()
    gui = UpdatingGUI(root)
    gui.pack()
    root.mainloop()

if __name__ == "__main__":
    main()

UPDATE

In response to the updated code: You are pretty much done, the only issue is that you are only calling the bargraph creator once, whereas it needs to be added to your update function like:

def update(self):
    # While the pipe has data, read and update the StringVar                                                                                
    while self.parent_pipe.poll():
        self.updating_int.set(self.parent_pipe.recv())
    dx = self.updating_int.get()
    self.area_barGraph.create_polygon(10,10, (10+dx),10, (10+dx),20, 10,20, outline='green', fill='yellow')
    # set the update method to run again in 1 seconds time                                                                                  
    self.parent.after(1000,self.update)

This will ensure that every time the intVar is updated the graph is also updated appropriately.

Upvotes: 2

Related Questions