Reputation: 1148
I am trying to build a Tkinter program that supports multiprocessing. I need to read from multiple Modbus devices and display the output onto the GUI.
I have successfully done this with a command line by using processes, but in Tkinter, my GUI freezes every time I attempt to read.
Here is my code:
import os
from multiprocessing import Process
import threading
import queue
import tkinter as tk
from tkinter import *
from tkinter import ttk
import time
import time as ttt
import minimalmodbus
import serial
minimalmodbus.CLOSE_PORT_AFTER_EACH_CALL = True
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.gas = minimalmodbus.Instrument('COM3', 1)
self.gas.serial.baudrate = 9600
self.gas.serial.parity = serial.PARITY_NONE
self.gas.serial.bytesize = 8
self.gas.serial.stopbits = 1
self.gas.serial.timeout = 0.25
self.gas.mode = minimalmodbus.MODE_RTU
self.pack()
self.create_widgets()
def create_widgets(self):
self.first_gas_labelframe = LabelFrame(self, text="Gas 1", width=100)
self.first_gas_labelframe.pack()
self.value_label = Label(self.first_gas_labelframe, text="Value")
self.value_label.pack()
self.unit_label = Label(self.first_gas_labelframe, text="Unit")
self.unit_label.pack()
self.temp_label = Label(self.first_gas_labelframe, text="Temp")
self.temp_label.pack()
self.timer_button = tk.Button(self, text='Start', command=self.process)
self.quit = tk.Button(self, text="QUIT", fg="red", command=root.destroy)
self.quit.pack()
self.gas_list = [self.gas]
def reader():
self.read = gas_num.read_registers(0,42)
self.value_label.config(text=self.read[0])
self.unit_label.config(text=self.read[1])
self.temp_label.config(text=self.read[2])
def process(self):
for sen in self.gas_list:
self.proc = Process(target=self.reader, args=(sen,))
self.proc.start()
self.proc.join()
if __name__ == '__main__':
root = tk.Tk()
app = Application()
app.mainloop()
When I press the start button the program will freeze until the process is done. How do I correctly set up a system to have the GUI operational while having processes run?
Upvotes: 3
Views: 3526
Reputation: 1706
The simplest solution would be to put this all on a separate thread. The reason your GUI freezes up is because the process
method has consumed the Main Thread and until it completes, Tkinter won't update anything.
Here's a simple example:
self.timer_button = tk.Button(self, text='Start', command=lambda: threading.Thread(target=self.process).start())
However, this doesn't stop the user from clicking the button twice. You could create a new method which controls this. Example:
import os
from multiprocessing import Process
import threading
import queue
import tkinter as tk
from tkinter import *
from tkinter import ttk
import time
import time as ttt
import minimalmodbus
import serial
minimalmodbus.CLOSE_PORT_AFTER_EACH_CALL = True
THREAD_LOCK = threading.Lock()
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.gas = minimalmodbus.Instrument('COM3', 1)
self.gas.serial.baudrate = 9600
self.gas.serial.parity = serial.PARITY_NONE
self.gas.serial.bytesize = 8
self.gas.serial.stopbits = 1
self.gas.serial.timeout = 0.25
self.gas.mode = minimalmodbus.MODE_RTU
self.pack()
self.create_widgets()
def create_widgets(self):
self.first_gas_labelframe = LabelFrame(self, text="Gas 1", width=100)
self.first_gas_labelframe.pack()
self.value_label = Label(self.first_gas_labelframe, text="Value")
self.value_label.pack()
self.unit_label = Label(self.first_gas_labelframe, text="Unit")
self.unit_label.pack()
self.temp_label = Label(self.first_gas_labelframe, text="Temp")
self.temp_label.pack()
self.timer_button = tk.Button(self, text='Start', command=self.start_thread)
self.quit = tk.Button(self, text="QUIT", fg="red", command=root.destroy)
self.quit.pack()
self.gas_list = [self.gas]
def check_thread(self):
if self.thread.is_alive():
root.after(50, self.check_thread)
else:
# Thread completed
self.timer_button.config(state='normal')
def start_thread(self):
self.timer_button.config(state='disabled')
self.thread = threading.Thread(target=self.process)
self.thread.start()
root.after(50, self.check_thread)
def reader(self):
self.read = gas_num.read_registers(0,42)
self.value_label.config(text=self.read[0])
self.unit_label.config(text=self.read[1])
self.temp_label.config(text=self.read[2])
def process(self):
with THREAD_LOCK:
for sen in self.gas_list:
self.proc = Process(target=self.reader, args=(sen,))
self.proc.start()
self.proc.join()
if __name__ == '__main__':
root = tk.Tk()
app = Application()
app.mainloop()
The Lock
is to ensure no two threads are running at the same time. I've also disables the button so that the user can't click until it's done. By using the root.after
method, you can create callbacks that wait for a period of time before running.
As far as the multiprocessing, you are running the processes on a separate process, but only one at a time. If you want to run many at one time, then you need to move the join
call somewhere else. I'm not sure how many processes are running at once but you could do something like this:
processes = []
for sen in self.gas_list:
proc = Process(target=self.reader, args=(sen,))
processes.append(proc)
proc.start()
[x.join() for x in processes]
In this implementation, I've removed assigning proc
as a class variable.
I do not have the libraries or data to properly test all this, but it should work...
This will initiate a pool of 6 processes that loop through self.gas_list
, passing the item to self.reader
. When those complete, it will check to make sure a second has gone by (waiting if it hasn't) and restarting the above process. This will run forever or until an exception is raised. You will need to import Pool
from the multiprocessing module.
def process(self):
with THREAD_LOCK:
pool = Pool(6)
while 1:
start_time = time.time()
pool.map(self.reader, self.gas_list)
execution_time = time.time() - start_time
if execution_time < 1:
time.sleep(1-execution_time)
Upvotes: 2