Python ProgressBar & GUI frozen while calculation for plot is going on - python

Can somebody help me out with threading in python and getting a progress bar working?
Even research gives quite a lot results, i cannot get it working.
I never did threading before and i cannot tell where to put things correctly.
For testing purposes i prepared a simple GUI with a button and a progress bar:
After clicking the button a simple 3d plot will pop-up.
Such plotting may take some computation time and while a user need to wait, id like to have the GUI not frozen and the progressbar animated.
At the moment the GUI freezes until the plot will show up. And after that the progress bar start the animation.
I have read a lot about threading and to put calculations and gui to different threads?
But im just to noobish to get it working.
Is somebody able to explain me more, direct me to similar problems or documentations?
Or maybe, in case is quickly solved, overtake the simple code and correct it the way it should be?
Thanks in advance for any kind of help.
Python script so far:
from time import sleep
from tkinter import EW
import ttkbootstrap as ttk
import numpy as np
import matplotlib.pyplot as plt
def main():
def plot_sample():
sleep(5) # simulate calculation times
x = np.outer(np.linspace(-2, 2, 30), np.ones(30))
y = x.copy().T # transpose
z = np.cos(x ** 2 + y ** 2)
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.plot_surface(x, y, z,cmap='viridis', edgecolor='none')
ax.set_title('Surface plot')
plt.show()
def progressbar_start():
progressbar.grid(row=1, column=0, sticky=EW, padx=10, pady=10) # let progressbar appear in GUI
progressbar.start(interval=10)
def progressbar_stop():
progressbar.stop()
progressbar.grid_forget() # hide progressbar when not needed
def button_clicked():
progressbar_start() # start progressbar before computation begins
plot_sample() # plotting
progressbar_stop() # stop progressbar after plot is done
# GUI
# window size and settings
root = ttk.Window()
# Basic settings for the main window
root.title('Test progressbar')
root.minsize(300, 200)
root.resizable(True, True)
root.configure(bg='white')
# grid settings for the main window in which LabelFrames are sitting in
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
root.rowconfigure(1, weight=1)
# Button fto show 3d-plot
button_calc_3dplot = ttk.Button(root, text='Calculate 3D Plot', command=button_clicked)
button_calc_3dplot.grid(row=0, column=0, padx=5, pady=5)
progressbar = ttk.Progressbar(style='success-striped', mode='indeterminate')
# end of GUI
root.mainloop()
if __name__ == "__main__":
main()

I have one solution. The main problem is, that You can't run the plt.show() function in a Thread. From this follows, that you can't run the window and the plt.show function at the same time.
from time import sleep
from tkinter import EW
import ttkbootstrap as ttk
import numpy as np
import matplotlib.pyplot as plt
from threading import Thread
x, y, z = None, None, None
def main():
def calculate_xyz():
sleep(5) # simulate calculation times
global x, y, z
x = np.outer(np.linspace(-2, 2, 30), np.ones(30))
y = x.copy().T # transpose
z = np.cos(x ** 2 + y ** 2)
def progressbar_start():
progressbar.grid(row=1, column=0, sticky=EW, padx=10, pady=10) # let progressbar appear in GUI
progressbar.start(interval=10)
def progressbar_stop():
progressbar.stop()
progressbar.grid_forget() # hide progressbar when not needed
def button_clicked():
progressbar_start() # start progressbar before computation begins
calculate_xyz_thread = Thread(target=calculate_xyz)
calculate_xyz_thread.start() # calculate x, y and z
root.after(500, wait_for_plotting) # wait for plotting and stop animation after finishing
def wait_for_plotting():
if x is None or y is None or z is None:
root.after(500, wait_for_plotting) # run self again
return # wait for calculation
# calculation is finished
progressbar_stop() # stop progressbar "after" plot is done
ax = plt.axes(projection='3d')
ax.plot_surface(x, y, z, cmap='viridis', edgecolor='none')
ax.set_title('Surface plot')
root.after(500, plot_3d)
def plot_3d():
plt.show()
# GUI
# window size and settings
root = ttk.Window()
# Basic settings for the main window
root.title('Test progressbar')
root.minsize(300, 200)
root.resizable(True, True)
root.configure(bg='white')
# grid settings for the main window in which LabelFrames are sitting in
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
root.rowconfigure(1, weight=1)
# Button fto show 3d-plot
button_calc_3dplot = ttk.Button(root, text='Calculate 3D Plot', command=button_clicked)
button_calc_3dplot.grid(row=0, column=0, padx=5, pady=5)
progressbar = ttk.Progressbar(style='success-striped', mode='indeterminate')
# end of GUI
root.mainloop()
if __name__ == "__main__":
main()

Related

tkinter plot auto rescale

I'm trying to create a GUI that will plot data from an array that is updated when I click a button (in this case move a slider), I can get the data to replot, however, the scale doesn't update and I can't work out how to do it. does anyone have any ideas?
the data is created in a function dataMaker and just returns 2 arrays, one of the time increments and the other of random data.
many thanks
import tkinter
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
# Implement the default Matplotlib key bindings.
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
import numpy as np
import dataMaker as DM
root = tkinter.Tk()
root.wm_title("Embedding in Tk")
fig = Figure(figsize=(5, 4), dpi=100)
rt_data = DM.comboP(1, 1000)
sample = rt_data[1]
torque = rt_data[2]
ax = fig.add_subplot()
line, = ax.plot(sample, torque)
canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea.
canvas.draw()
# pack_toolbar=False will make it easier to use a layout manager later on.
toolbar = NavigationToolbar2Tk(canvas, root, pack_toolbar=False)
toolbar.update()
canvas.mpl_connect(
"key_press_event", lambda event: print(f"you pressed {event.key}"))
canvas.mpl_connect("key_press_event", key_press_handler)
button_quit = tkinter.Button(master=root, text="Quit", command=root.destroy)
def update_frequency(new_val):
rt_data = DM.comboP(1, 1000)
sample = rt_data[1]
torque = rt_data[2]
line.set_data(sample, torque)
ax.autoscale_view()
# required to update canvas and attached toolbar!
canvas.draw()
slider_update = tkinter.Scale(root, from_=1, to=5, orient=tkinter.HORIZONTAL,
command=update_frequency, label="Frequency [Hz]")
# Packing order is important. Widgets are processed sequentially and if there
# is no space left, because the window is too small, they are not displayed.
# The canvas is rather flexible in its size, so we pack it last which makes
# sure the UI controls are displayed as long as possible.
button_quit.pack(side=tkinter.BOTTOM)
slider_update.pack(side=tkinter.BOTTOM)
toolbar.pack(side=tkinter.BOTTOM, fill=tkinter.X)
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
tkinter.mainloop()
I've searched the internet but can't find a solution, id like the scale the change to fit the data that's in the array

stopping NavigationToolbar2Tk from displaying coordinates

I'm trying to get NavigationToolbar2Tk to stop printing the coordinates in red shown below. I want to stop printing becaause the window resizes when the mouse moves over the figure.
My code follows. Even though Frame10 has grid_propagate(False) it resizes slightly when the mouse is moved over it. I want to stop that. My code is given below.
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,
NavigationToolbar2Tk)
from matplotlib.figure import Figure
import tkinter
root = tkinter.Tk()
root.wm_title('Stop toolbar coords')
root.grid_rowconfigure(0, weight=0)
root.grid_rowconfigure(1, weight=0)
root.grid_rowconfigure(2, weight=0)
root.grid_columnconfigure(0, weight=0)
Frame00 = tkinter.Frame(root,width='3.0i',height='3.0i')
Frame10 = tkinter.Frame(root,width='3.0i',height='1.0i')
Frame00.grid(row=0,column=0)
Frame00.grid_rowconfigure(0,weight=0)
Frame00.grid_columnconfigure(0,weight=0)
# stop frame from expanding to size of widget
Frame00.grid_propagate(False)
Frame10.grid(row=1,column=0)
Frame10.grid_rowconfigure(0,weight=0)
Frame10.grid_columnconfigure(0,weight=0)
# stop frame from expanding to size of widget
Frame10.grid_propagate(False)
# initialize matplotlib figure
fig = Figure(figsize=(2.5,2.5),dpi=100)
ax = fig.gca()
ax.plot([0.1,0.2,0.3],[0.5,0.6,0.7],'bo-')
ax.set_title('Title')
canvas = FigureCanvasTkAgg(fig,master=Frame00)
canvas.draw()
canvas.get_tk_widget().grid(row=0,column=0)
toolbar = NavigationToolbar2Tk(canvas,Frame10)
tkinter.mainloop()
The coordinates are displayed by calling the .set_message() method of the NavigationToolbar2Tk. One way to get rid of this behavior is to override this method:
class Toolbar(NavigationToolbar2Tk):
def set_message(self, s):
pass
# ...
toolbar = Toolbar(canvas, Frame10)

Is there a way to delay the on_changed() call for matplotlib sliders?

I am currently working to create a tkinter GUI which shows a matplotlib figure with two sliders. The sliders are to be used for selecting a range of data (they are connected to vertical lines on the plot).
The issue I am running into is that when I change the sliders quickly a few times the GUI freezes. I believe this is due to the on_changed() call I am using to make the sliders update. When you move the slider quickly I believe on_changed() is being called multiple times at a faster rate than the program can keep up with, thus causing the freeze.
Is there any way to add a delay to the on_changed() call so it only looks to see if the slider has been changed every given interval of time (ex. 100 ms)?
Below is example code I found which freezes in the way I described.
import tkinter as tk
from tkinter.ttk import Notebook
from tkinter import Canvas
from tkinter import messagebox as msg
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.widgets import Slider, Button, RadioButtons
class LukeOutline(tk.Tk):
def __init__(self):
# Inherit from tk.Tk
super().__init__()
# Title and size of the window
self.title('Luke Outline')
self.geometry('600x400')
# Create the drop down menus
self.menu = tk.Menu(self,bg='lightgrey',fg='black')
self.file_menu = tk.Menu(self.menu,tearoff=0,bg='lightgrey',fg='black')
self.file_menu.add_command(label='Add Project',command=self.unfinished)
self.menu.add_cascade(label='File',menu=self.file_menu)
self.config(menu=self.menu)
# Create the tabs (Graph, File Explorer, etc.)
self.notebook = Notebook(self)
graph_tab = tk.Frame(self.notebook)
file_explorer_tab = tk.Frame(self.notebook)
# Sets the Graph Tab as a Canvas where figures, images, etc. can be added
self.graph_tab = tk.Canvas(graph_tab)
self.graph_tab.pack(side=tk.TOP, expand=1)
# Sets the file explorer tab as a text box (change later)
self.file_explorer_tab = tk.Text(file_explorer_tab,bg='white',fg='black')
self.file_explorer_tab.pack(side=tk.TOP, expand=1)
# Add the tabs to the GUI
self.notebook.add(graph_tab, text='Graph')
self.notebook.add(file_explorer_tab, text='Files')
self.notebook.pack(fill=tk.BOTH, expand=1)
# Add the graph to the graph tab
self.fig = Figure()
graph = FigureCanvasTkAgg(self.fig,self.graph_tab)
graph.get_tk_widget().pack(side='top',fill='both',expand=True)
EllipseSlider(self.fig)
#------------------------------------------------------
def quit(self):
'''
Quit the program
'''
self.destroy()
#------------------------------------------------------
def unfinished(self):
'''
Messagebox for unfinished items
'''
msg.showinfo('Unfinished','This feature has not been finished')
#------------------------------------------------------
def random_graph(self):
x = list(range(0,10))
y = [i**3 for i in x]
fig = Figure()
axes = fig.add_subplot(111)
axes.plot(x,y,label=r'$x^3$')
axes.legend()
return fig
#----------------------------------------------------------
class EllipseSlider():
#------------------------------------------------------
def __init__(self,fig):
self.fig = fig
# Initial values
self.u = 0. #x-position of the center
self.v = 0. #y-position of the center
self.a = 2. #radius on the x-axis
self.b = 1.5 #radius on the y-axis
# Points to plot against
self.t = np.linspace(0, 2*np.pi, 100)
# Set up figure with centered axes and grid
self.ax = self.fig.add_subplot(111)
self.ax.set_aspect(aspect='equal')
self.ax.spines['left'].set_position('center')
self.ax.spines['bottom'].set_position('center')
self.ax.spines['right'].set_color('none')
self.ax.spines['top'].set_color('none')
self.ax.xaxis.set_ticks_position('bottom')
self.ax.yaxis.set_ticks_position('left')
self.ax.set_xlim(-2,2)
self.ax.set_ylim(-2,2)
self.ax.grid(color='lightgray',linestyle='--')
# Initial plot
self.l, = self.ax.plot(self.u+self.a*np.cos(self.t),
self.v+self.b*np.sin(self.t),'k')
# Slider setup
self.axcolor = 'lightgoldenrodyellow'
self.axb = self.fig.add_axes([0.25, 0.1, 0.65, 0.03], facecolor=self.axcolor)
self.axa = self.fig.add_axes([0.25, 0.15, 0.65, 0.03], facecolor=self.axcolor)
self.sb = Slider(self.axb, 'Y Radius', 0.1, 2.0, valinit=self.b)
self.sa = Slider(self.axa, 'X Radius', 0.1, 2.0, valinit=self.a)
# Call update as slider is changed
self.sb.on_changed(self.update)
self.sa.on_changed(self.update)
# Reset if reset button is pushed
self.resetax = self.fig.add_axes([0.8,0.025,0.1,0.04])
self.button = Button(self.resetax, 'Reset', color=self.axcolor, hovercolor='0.975')
self.button.on_clicked(self.reset)
# Color button setup
self.rax = self.fig.add_axes([0.025, 0.5, 0.15, 0.15], facecolor=self.axcolor)
self.radio = RadioButtons(self.rax, ('red', 'blue', 'green'), active=0)
self.radio.on_clicked(self.colorfunc)
#------------------------------------------------------
def update(self, val):
'''
Updates the plot as sliders are moved
'''
self.a = self.sa.val
self.b = self.sb.val
self.l.set_xdata(self.u+self.a*np.cos(self.t))
self.l.set_ydata(self.u+self.b*np.sin(self.t))
#------------------------------------------------------
def reset(self, event):
'''
Resets everything if reset button clicked
'''
self.sb.reset()
self.sa.reset()
#------------------------------------------------------
def colorfunc(self, label):
'''
Changes color of the plot when button clicked
'''
self.l.set_color(label)
self.fig.canvas.draw_idle()
#----------------------------------------------------------
if __name__ == '__main__':
luke_gui = LukeOutline()
luke_gui.mainloop()
es = EllipseSlider()
(UPDATE):
I have implemented the correction you showed and the code looks to work as intended except the vertical sliders I am using now do not update their position until I move the slider a second time. Please see the section from my code below:
# Call update if slider is changed
self.sa.on_changed(self.schedule_update)
self.sb.on_changed(self.schedule_update)
def update(self, value):
print(value)
self.vert_a.set_xdata(value)
self.vert_b.set_xdata(self.sb.val)
root.update()
def schedule_update(self, new_value):
if self.after_id:
root.after_cancel(self.after_id)
self.after_id = root.after(1000, self.update, new_value)
root = tk.Tk()
mainapp = MainApp(root)
root.mainloop()
Tkinter has a method named after for delaying the execution of a command. Your slider can use this to delay the effect of the slider for a second or two. If the slider moves, it can cancel the previously scheduled command and submit a new one.
Here's an example using plain tkinter for simplicity. Run the code and then move the slider quickly or slowly. You'll see that the label will only update after you haven't moved the slider for a full second.
import tkinter as tk
after_id = None
def schedule_update(new_value):
global after_id
if after_id:
root.after_cancel(after_id)
after_id = root.after(1000, update_label, new_value)
def update_label(new_value):
label.configure(text=f"New value: {new_value}")
root = tk.Tk()
label = tk.Label(root, text="", width=20)
scale = tk.Scale(
root, from_=1, to=100,
orient="horizontal", command=schedule_update
)
label.pack(side="top", fill="x", padx=20, pady=(10,0))
scale.pack(side="bottom", fill="x", padx=20, pady=10)
root.mainloop()

Matplotlib live graph update issue

I have a program that receives sensor data through the serial port of my laptop and then prints these values to a screen with a Tkinter gui and adds these values to an array for my matplotlib live graphs. The program works fine for a while but once it gets to the .pop(0) section, I get this error:
ValueError: x and y must have same first dimension, have shapes (5000,) and (5001,)
and no graph data shows when I click my Tkinter button.
I have changed this section if(cntB2>10000): to if(cntB2=>10000): but still no luck.
This is a stripped down version of my script. This will not work if you run it as all the Tkinter labels and placement take a lot of codeing and make it difficult to analyse the problem.
import matplotlib
# Main Tkinter application
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
root.title( "Stylibleue Dashboard")
root.state('zoomed')
root.configure(background='black')
self.grid(row=0, column=0, sticky="nsew") # Thus makes an invisible grid to make the layout
self.grid_rowconfigure(0, weight=3)
self.grid_columnconfigure(0, weight=3)
self.columnconfigure(1, weight=3)
def createWidgets(self):
#B1 Labels --------------------------
self.temperature = Label(self, text="Bassin 1", font=('Verdana', 15), fg="red", bg="black").grid(row=3, column=0, sticky="nsew")
################ This is the Graph page when button is clicked
def button_press2(self):
clicked = True
print (clicked)
drawnow(self.makeFig2) #Call drawnow to update our live graph
plt.pause(.000001) #Pause Briefly. Important to keep drawnow from crashing
def makeFig2(self):
mng = plt.get_current_fig_manager()
mng.window.state('zoomed')
plt.title("D.O graph bassin 2") #Plot the title
plt.grid(True) #Turn the grid on
plt.setp(plt.xticks()[1], rotation=30, ha='right') #ha is the same as horizontalalignment
plt.ylabel('D.O') #Set ylabels
plt.plot(dt2,Oxy2, label='D.O') #plot the temperature
plt.legend(loc='upper left') #plot the legend
plt.draw()
########## This takes the sensor reading
def measure(self):
if com_port_connected == True:
try:
# If there is incoming data to parse and display it
if(arduinoData.inWaiting()>0 ): #and arduinoData.inWaiting()>50 ):
arduinoString = arduinoData.readline()
self.B10.set("")
arduinoString =str(arduinoString) #removes the surrounding rubbish
aaa = arduinoString.split(',')
a = sys.getsizeof(arduinoString)
print("-------Received Sensor data-------")
print(a)
self.B10.set("")
arduinoString =str(arduinoString) #removes the surrounding rubbish
aaa = arduinoString.split(',')
print(arduinoString.split(','))
a = sys.getsizeof(arduinoString)
#################### This updates the GUI labels and adds to the graph array
if aaa[1] == "B2" and Preceed == True:#BASSIN TWO ADDRESS HERE
self.B2.set(str(aaa[3]))
self.B22.set(str(aaa[5]))
self.B22222.set(str(datetime.datetime.now().strftime("%d/%m %H:%M")))
print(datetime.datetime.now().strftime("%d/%m %H:%M"))#%Y-%m-%d %H:%M:%S
try:
tempF2.append(float(aaa[5]))
Oxy2.append(float(aaa[3]))
dt2.append(datetime.datetime.now())
global cntB2
cntB2=cntB2+1
print("cntB2")
print(cntB2)
if(cntB2>10000):
tempF2.pop(0)
Oxy2.pop(0)
dt2.pop(0)
except:
print("Error could not append sensor data")
except:
print("No data received")
self.B10.set("Connect RX")
self.after(1000,self.measure) # Wait 1 second between each measurement
self.after(1000,self.check_buttons) # check for button press
arduinoData.flushOutput()
arduinoData.flushInput()
# Create and run the GUI
root = Tk()
app = Application(master=root)
app.mainloop()

Pickable matplotlib plot in tkk frame

I would like to have a GUI where one part of the interface contains a plot and the rest of the window some tools to work around the plot.
I would like to use mpl_connect to connect the matplotlib canvas with the tkk frame so that I can choose points in the plot to work with.
This was my try, which cowardly refuses to do what I think it should do:
import Tkinter as tk
import ttk
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2TkAgg)
import matplotlib.pyplot as plt
import numpy as np
class Frame_examples_program():
def __init__(self):
self.window = tk.Tk()
self.window.title("Amazing GUI 5000")
self.create_widgets()
def create_widgets(self):
self.window['padx'] = 10
self.window['pady'] = 10
# - - - - - - - - - - - - - - - - - - - - -
# Frame
frame1 = ttk.Frame(self.window, relief=tk.RIDGE)
frame1.grid(row=0, column=0, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)
frame2 = ttk.Frame(self.window, relief=tk.RIDGE)
frame2.grid(row=1, column=0, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)
self.PlotFrame(frame1, frame2)
class PlotFrame():
# The plot
def __init__(self, parent1, parent2):
self.parent1 = parent1
self.parent2 = parent2
canvas = self.plot()
self.plot_toolbar(canvas)
def plot(self):
# the actual plot
fig, ax = plt.subplots()
plt.imshow(np.ones((100,100)),picker=True)
canvas = FigureCanvasTkAgg(fig, self.parent1)
canvas.mpl_connect('button_press_event', self.onclick)
return(canvas)
def plot_toolbar(self, canvas):
# the tool bar to the plot
toolbar = NavigationToolbar2TkAgg(canvas, self.parent2)
toolbar.update()
canvas.get_tk_widget().grid(row=1, column=1)
canvas.draw()
def onclick(self, event):
# the devilish thing that does nothing!
print('WOHOOOO')
# Create the entire GUI program
program = Frame_examples_program()
# Start the GUI event loop
program.window.mainloop()
As you'll see when you run this, the matplotlib toolbar nicely works, but I just can't call the onclick event! Why?
The PlotFrame instance that is created via self.PlotFrame(frame1, frame2) is not stored anywhere and hence garbage collected. At the point where you expect the callback to happen this instance does not exist in memory anymore.
Solution: Make sure to keep a reference to the PlotFrame at all time, e.g.
self.myplot = self.PlotFrame(frame1, frame2)
Note that this is a more or less general rule: You would almost never instantiate a class without storing it anywhere. In case you do and don't run into trouble, that would mostly be sign that the class is not needed at all.

Categories

Resources