Update tkinter/matplotlib python without mainloop() - python

Is there a way to manually update Tkinter plots from another file?
I have another file that needs to be continuously polling (while True), so I believe I can't have a mainloop in the plots class since it would block my main class. Is there any way I can call the method "plot_data" from this other file and have the graphs (a,b,c,d) update live? Currently the frame freezes and live updates aren't seen.
However it seems to update in real time when the program is being run in debug mode. Is there a reason why the behavior in execution is different between RUN/ DEBUG in Pycharm?
Below is a view of the Plots class:
class Plots(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.wm_title(self, "GUI")
# TODO Make sure 'zoomed' state works on all laptops
self.state('zoomed') # Make fullscreen by default
container = tk.Frame(self, bg='blue')
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
frame = GraphPage(container)
self.page = GraphPage
frame.configure(background='black')
self.frame = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame() # Display frame on window
def show_frame(self):
frame = self.frame
frame.tkraise()
def plot_data(self):
# TODO Add exception handling for opening the file
file = open("../storage/data.csv", "r") # Open data file for plotting
t_list = ...
a_list = ...
v_list = ...
a2_list = ...
a.clear()
a.plot(time_list, t_list)
a.set_ylabel('T')
b.clear()
b.plot(time_list, a_list)
b.set_ylabel('A')
c.clear()
c.plot(time_list, v_list)
c.set_ylabel('V')
d.clear()
d.plot(time_list, a2_list)
d.set_xlabel('Time(s)')
d.set_ylabel('A')
class GraphPage(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
label = tk.Label(self, bg='red', text="GUI", font=LARGE_FONT, width=400)
label.pack(pady=10, padx=10)
canvas = FigureCanvasTkAgg(f, self)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
The other file is essentially just:
while(true):
plots.plot_data()

Related

Why VS code not showing function from parent class object in python

I have a question about why my child class can't see parents method(it can't see them, but it can use them). I'm creating a GUI in tkinter and I need to use some methods from my parent window.
My parent class look like this:
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")
# the container is where we'll stack a bunch of frames
# on top of each other, then the one we want visible
# will be raised above the others
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
manager_of_action = Action_manager()
self.frames = {}
for F in (MenuView, LoginView, GameView, ResultView ):
page_name = F.__name__
frame = F(parent=container, controller=self,action_manager= manager_of_action)
self.frames[page_name] = frame
# put all of the pages in the same location;
# the one on the top of the stacking order
# will be the one that is visible.
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("MenuView")
def show_frame(self, page_name):
'''Show a frame for the given page name'''
frame = self.frames[page_name]
frame.tkraise()
and child class look like this:
class MenuView(tk.Frame):
def __init__(self, parent, controller, action_manager):
tk.Frame.__init__(self, parent)
self.controller = controller
button1 = tk.Button(self, text="Multiplayer",command=lambda: controller.show_frame("LoginView"),height=3,width=6, bg=("white"), font=controller.title_font)
button2 = tk.Button(self, text="Exit",command=lambda: exit(0),height=3,width=6,bg=("white"), font=controller.title_font )
button1.pack(side="top", fill="both", expand=True)
button2.pack(side="top", fill="both", expand=True)
But VS Code can't see .title_font() and .show_frame() but if I run my program, it is running as it should. Why it's happening and can I somehow fix it? (If I would have 50 methods in my parent class, I don't want to always look back to parent class and copy + paste my desired fuction.)
Thanks a lot for every help.
EDIT:
VScode doesn't highlight .title_font() and .show_frame() as it can be seen on picture. This means that VS code is not showing them in IntelliSense (Pylance) menu (. + space menu that will pop up after calling some object). Normally, functions are highlighted in yellow.
no highlight for .show_frame, .title_font

How to access methods from a parent class which enables the frame swapping?

This is the window provides the container and methods which allow frame swapping:
class Login_Window(ctk.CTk):
def __init__(self, *args, **kwargs):
super().__init__()
self.geometry('400x400')
self.title('Music Mayhem')
self.resizable(False, False)
container = ctk.CTkFrame(master=self)
container.pack(side='top', fill='both', expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames={}
for F in (LoginFrame, RegEmailFrame):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky= 'nsew')
frame.grid_columnconfigure(0,weight=1)
frame.grid_rowconfigure(0,weight=1)
self.show_frame(LoginFrame)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
In order to swap the frames, a button has to be created in the frame that is going to be swapped. How would i go about creating an instance of another class within these frames which will also call the show_frame method? Here is the code for the frames- this could be ran as long as you have tkinter and custom tkinter installed. The only aspect that should supposedly not work are the buttons in the menu frame.
Yes, in this situation the menu frame is not needed but this is just a simple example because the actual code is way too long to be included here.
I have tried adding the menu frame into the list of frames to be swapped (in the class above) and giving it the same parent and controller attributes as the other frame but that required a parent and controller argument to be passed through when it is called in the Login and Register frames.
Is there a way to get round this or a simpler method that could be implemented instead?
class LoginFrame (tk.Frame):
def __init__(self,parent, controller):
tk.Frame.__init__(self, parent)
self.menu = Menu(self)
self.menu.grid(row=0, column=0)
self.loginBtn = ctk.CTkButton(master=self, width=100, height = 20,text='Login',
state='normal',
command=lambda: controller.show_frame(RegEmailFrame)
self.loginBtn.grid(row=1, column=0)
class RegEmailFrame(tk.Frame):
def __init__(self, parent, controller,header_name="Register Email"):
tk.Frame.__init__(self, parent)
self.menu = Menu(self)
self.menu.grid(row=0, column=0)
self.emailLabel = ctk.CtKLabel(master=self,width=100, height=20 text='Frame swapped')
self.emailLabel.grid(row=1, column=0)
class Menu(tk.Frame):
def __init__(self, *args, header_name="Logo Frame",
width=175, height=175,**kwargs):
super().__init__(*args, width=width, height=height, **kwargs)
self.menuloginBtn = ctk.CTkButton(master=self, width=100, height = 20,text='Login',
state='normal',
command=lambda: controller.show_frame(LoginFrame)
self.menuloginBtn.grid(row=0, column=0)
self.menuRegBtn = ctk.CTkButton(master=self, width=100, height = 20,text='Login',
state='normal',
command=lambda: controller.show_frame(RegEmailFrame)
self.menuRegBtn.grid(row=1, column=0)
In the current implementation, the Menu class does not have access to the controller object that is used to switch between frames in the Login_Window class. One way to fix this would be to pass the controller object to the Menu class during instantiation.
You can do this by adding a parameter called controller in the Menu class constructor and then passing it as an argument when creating an instance of the Menu class in the LoginFrame and RegEmailFrame classes.
For example, in the LoginFrame class:
def __init__(self,parent, controller):
tk.Frame.__init__(self, parent)
self.menu = Menu(self, controller)
self.menu.grid(row=0, column=0)
And in the Menu class constructor:
def __init__(self, parent, controller, *args, header_name="Logo Frame",
width=175, height=175,**kwargs):
super().__init__(parent, *args, width=width, height=height, **kwargs)
self.controller = controller
With this changes, the Menu class now has access to the controller object and can use it to switch between frames using the show_frame method.
You should also make the same changes in the RegEmailFrame class and in the constructor of the Menu class.
Hope this helps!

Update tkinter progress-bar from outside of main function/class

I am new to Python and not very experienced with classes, however working on the creation of a tkinter GUI for data processing right now.
As many time consuming processes are happening in the background not visible for the user, I would like to insert a progress-bar that shows the current progress between 0 and 100 as Progress and the processing step Action in the main window
Right now, I have problems to access the bar parameters (value and label/name) outside of the class when the code is doing the data processing in a different function.
Below is a working example of the GUI in Python 3.7
import time
import tkinter as tk
from tkinter import ttk
def ProcessingScript():
### UpdateProgressbar(50, 'Halfway there') ###
time.sleep(2)
print('Processing takes place here')
### UpdateProgressbar(75, 'Finishing up') ###
time.sleep(2)
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self, width=500, height=500)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.geometry("500x500")
self.frames = {}
frame = ProcessingPage(container, self)
self.frames[ProcessingPage] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(ProcessingPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class ProcessingPage(tk.Frame):
def __init__(self, parent, controller, ):
tk.Frame.__init__(self, parent)
self.controller = controller
def PlotData():
UpdateProgressbar(10, 'Generating...')
# Execute Main Plotting Function here
ProcessingScript()
UpdateProgressbar(100, 'Finished Plot')
def UpdateProgressbar(Progress, Action):
progressLabel = tk.Label(self, text=Action).place(x=20, y=440)
progressBar['value'] = Progress
progressBar = ttk.Progressbar(self, orient="horizontal", length=200,mode="determinate")
progressBar.place(x=20, y=470)
progressBar['value'] = 0
progressLabel = tk.Label(self, text='Idle...').place(x=20, y=440)
PlotButton = tk.Button(self, text='Plot Data',command= PlotData)
PlotButton.place(x=20, y=320)
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
In this example, the ProcessingScript function would be in a different file and as an ideal outcome I would like to be able to call the UpdateProgressbar function from anywhere in my other scripts to update the bar.
Note: I am aware that a function inside the __init__ function is not best practice, however I was not able to get it running in any other way as I found no way to connect the results of the UpdateProgressbar function with the progressBar created.
Any help to achieve this and exclude UpdateProgressbar from __init__ is much appreciated.
EDIT:
Below is a working version based on the input from the comments. It might now be very pretty but is currently doing what I expect it do to. Please let me know if you see some possibilities for improvement.
app.update() has to be called after each change in the progress bar to show the error and old labels are deleted with self.progressLabel.destroy().
timeit.sleep() is simply a way of showing the changes and will not be part of the final code.
import time
import tkinter as tk
from tkinter import ttk
def ProcessingScript(self, callback):
ProcessingPage.UpdateProgressbar(self, 50, 'Halfway there')
time.sleep(2)
print('Processing takes place here')
ProcessingPage.UpdateProgressbar(self, 75, 'Finishing up')
time.sleep(2)
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self, width=500, height=500)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.geometry("500x500")
self.frames = {}
frame = ProcessingPage(container, self)
self.frames[ProcessingPage] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(ProcessingPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class ProcessingPage(tk.Frame):
def __init__(self, parent, controller, ):
tk.Frame.__init__(self, parent)
self.controller = controller
progressBar = ttk.Progressbar(self, orient="horizontal", length=200,mode="determinate")
progressBar.place(x=20, y=470)
progressBar['value'] = 0
self.progressLabel = tk.Label(self, text='Idle...')
self.progressLabel.place(x=20, y=440)
PlotButton = tk.Button(self, text='Plot Data',command= self.PlotData)
PlotButton.place(x=20, y=320)
def PlotData(self):
self.UpdateProgressbar(10, 'Generating...')
app.update()
time.sleep(2)
# Execute Main Plotting Function here
ProcessingScript(self, self.UpdateProgressbar)
self.UpdateProgressbar(100, 'Finished Plot')
app.update()
def UpdateProgressbar(self, Progress, Action):
self.progressLabel.destroy()
self.progressLabel = tk.Label(self, text=Action)
self.progressLabel.place(x=20, y=440)
progressBar = ttk.Progressbar(self, orient="horizontal", length=200,mode="determinate")
progressBar.place(x=20, y=470)
progressBar['value'] = Progress
app.update()
if __name__ == "__main__":
app = SampleApp()
app.mainloop()

Calling a widget from another class to change its properties in tKinter

I have made a function in the main constructor of my tKinter app which updates certain properties of widgets e.g. their text across multiple frames. What I'm trying to do is change widgets in multiple frames at the same time while in a controller frame.
def update_widgets(self, frame_list, widget_name, criteria, output):
for i in frame_list:
i.widget_name.config(criteria=output)
# update_widgets(self, [Main, AnalysisSection], text_label, text, "foo")
# would result in Main.text_label_config(text="foo") and
# AnalysisSection.text_label_config(text="foo") ideally.
However with this code, I'm encountering two problems. Firstly, I'm getting an attribute error stating that both frames don't have the attribute widget_name. Secondly, when I tried to refer to the widget names with the self prefix, both frames say they don't have the attribute self. Is there a way to fix this?
Full program below:
import tkinter as tk
class Root(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.frames = {}
container = tk.Frame(self)
container.pack(side="bottom", expand=True)#fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
for X in (A, B):
frame=X(container, self)
self.frames[X]=frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(A)
def show_frame(self, page):
frame = self.frames[page]
frame.tkraise()
def update_widgets(self, frame_list, widget_name, criteria, output):
for i in frame_list:
frame = self.frames[i]
widget = getattr(frame, widget_name)
widget[criteria] = output
class A(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.text = 'hello'
self.classLabel = tk.Label(self, text="Frame A")
self.classLabel.pack(side=tk.TOP)
# trying to change this widget
self.wordLabel = tk.Label(self, text="None")
self.wordLabel.pack(side=tk.TOP)
self.changeTextLabel = tk.Label(self, text="Change text above across both frames").pack(side=tk.TOP)
self.changeTextEntry = tk.Entry(self, bg='pink')
self.changeTextEntry.pack(side=tk.TOP)
self.changeFrameButton = tk.Button(text="Change to Frame B", command=lambda: self.controller.show_frame(B))
self.changeFrameButton.pack(side=tk.TOP, fill=tk.X)
self.changeTextEntryButton = tk.Button(self, text="ENTER", width=5, command=lambda: self.controller.update_widgets([A, B], 'self.wordLabel', 'text', self.changeTextEntry.get()))
self.changeTextEntryButton.pack(side=tk.TOP, fill=tk.X)
### calling this function outside of the button; this is already
### called within a function in my project.
x = self.controller.update_widgets([A, B], 'wordLabel', 'text', '*initial change*')
class B(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.text = 'hello'
self.classLabel = tk.Label(self, text="Frame B")
self.classLabel.pack(side=tk.TOP)
# trying to change this widget
self.wordLabel = tk.Label(self, text="None")
self.wordLabel.pack(side=tk.TOP)
self.changeTextLabel = tk.Label(self, text="Change text above across both frames").pack(side=tk.TOP)
self.changeTextEntry = tk.Entry(self, bg='light yellow').pack(side=tk.TOP)
self.changeFrameButton = tk.Button(text="Change to Frame A", command=lambda: self.controller.show_frame(A))
self.changeFrameButton.pack(side=tk.TOP, fill=tk.X)
self.changeTextEntryButton = tk.Button(self, text="ENTER", width=5, command=lambda: self.controller.update_widgets([A, B], 'self.wordLabel', 'text', self.changeTextEntry.get()))
self.changeTextEntryButton.pack(side=tk.TOP, fill=tk.X)
if __name__ == '__main__':
app = Root()
The problem in your code is that you're trying to get an attribute of a class rather than an instance of a class. You need to convert i to the actual instance of that class. You have the additional problem that you're passing 'self.wordLabel' rather than just 'wordLabel'.
A simple fix is to look up the instance in self.frames
def update_widgets(self, frame_list, widget_name, criteria, output):
for i in frame_list:
frame = self.frames[i]
label = getattr(frame, widget_name)
label[criteria] = output
You also need to change the button command to look like this:
self.changeTextEntryButton = tk.Button(... command=lambda: self.controller.update_widgets([A,B], 'wordLabel', 'text', self.changeTextEntry.get()))
If you intend for update_widgets to always update all of the page classes, there's no reason to pass the list of frame classes in. Instead, you can just iterate over the known classes:
def update_widgets(self, widget_name, criteria, output):
for frame in self.frames.values():
label = getattr(frame, 'classLabel')
label[criteria] = output
You would then need to modify your buttons to remove the list of frame classes:
self.changeTextEntryButton = tk.Button(..., command=lambda: self.controller.update_widgets('wordLabel', 'text', self.changeTextEntry.get()))

How can I evenly divide space between two frames using the grid geometry manager in tkinter?

I have a tkinter application I'm trying to create. I've deiced to use the grid geometry manger instead of pack. It has been working well so far, but I've come across a strange problem.
The basic layout I want in my application is to have a toolbar frame on the top side of the window, which will expand to fill the window horizontally, and take up 1/5 of the screen space in terms of height. Another frame will fill the reaming 4/5 screen space(again in terms of height), and will also fill the screen horizontally. Another third frame will be under the second, and I will use frame.tkraise() to switch between the two.
Here is a minimal example of what I'm trying to accomplish:
import tkinter as tk
class Toolbar(tk.Frame):
def __init__(self, parent, root, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.root = root
self.lbl = tk.Label(self, text='Toolbar')
self.lbl.pack()
class PageOne(tk.Frame):
def __init__(self, parent, root, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.root = root
self.lbl = tk.Label(self, text='PageOne')
self.lbl.pack()
self.btn = tk.Button(self, text='MainPage', command=lambda: self.root.show_frame(MainPage))
self.btn.pack()
class MainPage(tk.Frame):
def __init__(self, parent, root, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.root = root
self.lbl = tk.Label(self, text='MainPage')
self.lbl.pack()
self.btn = tk.Button(self, text='PageOne', command=lambda: self.root.show_frame(PageOne))
self.btn.pack()
class App(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._setup_window()
self.frames = {}
container = tk.Frame(self, bg='yellow')
container.pack(side='top', fill='both', expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
container.grid_rowconfigure(1, weight=3)
self.toolbar = Toolbar(container, self, bg='red')
self.toolbar.grid(row=0, column=0, sticky='new')
for page in (MainPage, PageOne):
frame = page(container, self, bg='blue')
self.frames[page] = frame
frame.grid(row=1, column=0, sticky='nsew')
self.show_frame(MainPage)
def _setup_window(self):
self.geometry('420x280')
self.title('backup')
def show_frame(self, page_name):
frame = self.frames[page_name]
frame.tkraise()
if __name__ == '__main__':
app = App()
app.mainloop()
The code above displays the following frame:
The yellow frame is the window which holds the tool bar and two other frames. The red frame is the tool bar. And the blue frames are the two main windows, which can be toggled between using the "MainPage" and "PageOne" buttons respectively.
As you can probably tell, my problem is that the tool bar frame(the red part)is not fully expanding to fill the reaming yellow space. No matter how much space I allocate for the tool bar frame, it does not expand.
My understanding was that if I put the tool bar frame in the zeroth column, and the
two main windows in the first, and I did container.grid_rowconfigure(0, weight=1) and container.grid_rowconfigure(1, weight=4) that would meet my needs. I made this assumption based on this excerpt from here:
To make a column or row stretchable, use this option and supply a value that gives the relative weight of this column or row when distributing the extra space. For example, if a widget w contains a grid layout, these lines will distribute three-fourths of the extra space to the first column and one-fourth to the second column:
w.columnconfigure(0, weight=3)
w.columnconfigure(1, weight=1)
If this option is not used, the column or row will not stretch.
Was I wrong to assume this? Is there something I'm missing? How can I make my tool bar frame expand to fill the extra space?
You just need to tell your toolbar to stick to the south side of the available space as well:
self.toolbar.grid(row=0, column=0, sticky='nesw')

Categories

Resources