I started from a tkinter app with a button starting a specific method from which data could be plotted with matplotlib on the gui.
However I needed to be able to cancel that method, so I changed up the code to use multiprocessing, which worked as expected. The problem is that the plotting is not done anymore.
In the beginning I thought I could just pass the axes as arguments and plot inside the target function, but that didn't give any results, so my next idea was to have the points that i want to plot be returned from the target process and plot them outside. That, I think, would imply not using join(), since I don't want any changes in the process flow. I tried using Queue and Array from multiprocessing but without success, probably i was missing something in the logic.
So my question is, what would be the correct approach between the 2 mentioned, and the correct code extension to what I already have.
The code snippets that are involved:
imports
from tkinter import *
from tkinter import messagebox
from tkinter import ttk
from tkinter import filedialog
from multiprocessing import Process, Value, Manager, Queue
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
main:
if __name__ == "__main__":
root = Tk()
ParId(root)
root.mainloop()
the ParId class:
class ParId(Frame):
def __init__(self, parent, *args, **kwargs):
Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
frame2 = LabelFrame(self.parent, padx = 5, pady = 1)
frame2.grid(row=0, column=1, padx = 10, pady = 1)
frame2.rowconfigure(0, weight = 1)
frame2.rowconfigure(1, weight = 1)
frame2.columnconfigure(0,weight = 1)
frame2.columnconfigure(1,weight = 1)
lf = self.create_left_frame(frame2)
kct_f = self.create_kct_frame(frame2)
lf.grid(column=0, row=0, sticky = "W")
kct_f.grid(row=1, column=0)
def create_left_frame(self, container):
frame = ttk.Frame(container)
start_button = Button(frame, text = "Continue", state = NORMAL, padx = 50, command = self.start_func, pady = 5)
start_button.grid(row = 3, column=0, padx=0, pady=10, sticky = "W")
return frame
def create_kct_frame(self,container):
frame = ttk.Frame(container)
self.figure_kct = plt.Figure(figsize=(4,3), dpi=100)
self.ax_kct = self.figure_kct.add_subplot(111)
self.ax_kct.grid()
kct_plot = FigureCanvasTkAgg(self.figure_kct, frame)
kct_plot.get_tk_widget().grid(pady = 10, padx = 10, row = 0, column = 0 ,sticky = "N")
return frame
# function that starts process
def start_func(self):
proc = Process(target=myfoo, args=(various args))
process_window = ProcessWindow(self, proc)
process_window.launch()
the ProcessWindow class:
class ProcessWindow(Toplevel):
def __init__(self, parent, process):
Toplevel.__init__(self, parent)
self.parent = parent
self.process = process
labl = Label(self, text = "in progress...")
terminate_button = ttk.Button(self, text="Cancel", command=self.cancel)
labl.grid(row=0,column=0)
terminate_button.grid(row=1, column=0)
def cancel(self):
self.process.terminate()
self.destroy()
messagebox.showinfo(title="Cancelled", message='Process was terminated')
def launch(self):
self.process.start()
self.after(10, self.isAlive)
def isAlive(self):
if self.process.is_alive():
self.after(100, self.isAlive)
elif self:
# Process finished
messagebox.showinfo(message="analysis complete", title="Finished")
self.destroy()
target method:
def myfoo(various arguments):
all_t = []
all_v = []
# rest of the function that populates all_t and all_v, which i want to plot on the main gui
As a summary, I want to plot on the gui(that uses tkinter &matplotlib) some points that are created inside an external function myfoo, but only after the process is finished, without losing the ability to cancel it. I am working on windows, python 3.7. Any insights are helpful.
CodePudding user response:
I'll show the way i solved it in here:
So as Aaron pointed out, i went on and used Queue, passed to both the process and the ProcessWindow class. Whatever was in myfoo was put() in the queue, and only when the process finished i get(), with a timeout, from the Queue, being careful how many times i use the get().
For the actual plotting, i just made a new method in the ParId, that updates the ax and figure, and can call that from the ProcesWindow since ParId is its parent.
I'm not sure if this is the best method, and also if there any any problems if i use the queue like this, memory-wise, if it should be specifically closed/cleared somehow.
from tkinter import *
from tkinter import messagebox
from tkinter import ttk
from tkinter import filedialog
import time
from multiprocessing import Process, Queue
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class ParId(Frame):
# function that starts process
def __init__(self, parent, *args, **kwargs):
Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
frame2 = LabelFrame(self.parent, padx = 5, pady = 1)
frame2.grid(row=0, column=1, padx = 10, pady = 1)
frame2.rowconfigure(0, weight = 1)
frame2.rowconfigure(1, weight = 1)
frame2.columnconfigure(0,weight = 1)
frame2.columnconfigure(1,weight = 1)
lf = self.create_left_frame(frame2)
kct_f = self.create_kct_frame(frame2)
lf.grid(column=0, row=0, sticky = "W")
kct_f.grid(row=1, column=0)
def start_func(self):
self.qu = Queue()
proc = Process(target=myfoo, args = (self.qu,))
self.process_window = ProcessWindow(self, proc, self.qu)
self.process_window.launch()
def create_left_frame(self, container):
frame = ttk.Frame(container)
start_button = Button(frame, text = "Continue", state = NORMAL, padx = 50, command = self.start_func, pady = 5)
start_button.grid(row = 3, column=0, padx=0, pady=10, sticky = "W")
return frame
def create_kct_frame(self,container):
frame = ttk.Frame(container)
self.figure_kct = plt.Figure(figsize=(4,3), dpi=100)
self.ax_kct = self.figure_kct.add_subplot(111)
self.ax_kct.grid()
kct_plot = FigureCanvasTkAgg(self.figure_kct, frame)
kct_plot.get_tk_widget().grid(pady = 10, padx = 10, row = 0, column = 0 ,sticky = "N")
return frame
def update_kct_frame(self, all_vel, all_time):
print("in the update kct")
self.ax_kct.plot(all_time,all_vel, '*')
self.figure_kct.canvas.draw()
class ProcessWindow(Toplevel):
def __init__(self, parent, process, qu):
Toplevel.__init__(self, parent)
self.parent = parent
self.process = process
self.qu = qu
labl = Label(self, text = "in progress...")
terminate_button = ttk.Button(self, text="Cancel", command=self.cancel)
labl.grid(row=0,column=0)
terminate_button.grid(row=1, column=0)
def cancel(self):
self.process.terminate()
self.destroy()
messagebox.showinfo(title="Cancelled", message='Process was terminated')
def launch(self):
self.process.start()
self.after(10, self.isAlive)
def isAlive(self):
if self.process.is_alive():
self.after(100, self.isAlive)
elif self:
# Process finished
self.all_vel = self.qu.get(timeout = 10)
self.all_time = self.qu.get(timeout = 10)
print("process finished, v is ", self.all_vel)
print("process finished, t is", self.all_time)
self.parent.update_kct_frame(self.all_vel, self.all_time)
messagebox.showinfo(message="analysis complete", title="Finished")
self.destroy()
def myfoo(qu):
all_t = []
all_v = []
for i in range(5):
all_t.append(1000*i)
all_v.append(10*i)
time.sleep(1)
print("i am in the target, appending", all_t[i])
qu.put(all_v)
qu.put(all_t)
if __name__ == "__main__":
root = Tk()
ParId(root)
root.mainloop()