Dynamically instantiate pages Tkinter - python

I'm building a GUI with code that originally came from another stack exchange answer. I've modified it since I want to pass variables to following pages and have them display the values. As a way to do this, rather than display on a button event, I am trying to create the page with the show() method:
import Tkinter as tk
#import vpnRename12 as Rename
#import vpnRename_pullconfig as pullConfig
"""
Create multiple pages to go through in the same frame. Use lift() to bring
the desired page into view and stack them on top of one another. Define
page-specific methods in each page.
"""
class vpnRenameProgram(tk.Tk):
"""
Create a page class that will allow you to lift to front the applicable
page with the proper functions to keep the script running.
"""
def __init__(self,*args,**kwargs):
tk.Tk.__init__(self,*args,**kwargs)
# Create an empty dictionary to contain all of the page names
self.frames = {}
self.show("MainView")
# Instantiate the next page on call, rather than at start to allow
flexibility
def instantiate_page(self, cls):
"""
Since all of the pages are defined in the same scope/namespace, we
can use
the globals()[] dict to find and instantiate the pages dynamically
with the show() method.
cls is the class argument we are doing a lookup on in the global()
dict.
"""
try:
newframe = globals()[cls](container,self)
page_name = newframe.__name__
except:
print("\nError defining inline class %s"%cls)#("Class %s is not defined" %cls)
newframe = None
page_name=globals()[cls].__name__
return newframe, page_name
# Create lift function to bring desired page to front of view,
#instantiate page if
# it isn't already (check frames dict)
def show(self, cls):
if cls not in self.frames.keys():
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
frame, page_name = self.instantiate_page(cls)
if frame==None:
frame = globals()[cls](parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=0, column=0, sticky="news")
frame = self.frames[cls]
frame.lift()
def get_page(self, classname):
"""
Return instance of page when it's class name is passed in as string
"""
for page in self.frames.values():
if str(page.__class__.__name__) == classname:
return page
return None
class MainView(tk.Frame):
def __init__(self,parent, controller,**kwargs):
tk.Frame.__init__(self,parent)
self.controller = controller
self.edit_directory="edit_dir"
self.complete_directory ="comp_dir"
TitleLabel = tk.Label(self, text="VPN Script Editor")
TitleLabel.pack({"side":"top"})
EditLabel = tk.Label(self, text="Edit File Directory:
%s"%self.edit_directory)
EditLabel.pack()
CompLabel = tk.Label(self, text="Completed File Directory:
%s"%self.complete_directory)
CompLabel.pack()
Next = tk.Button(self, text="Next", command=lambda:
controller.show("listVPN"))
Next.pack()
class listVPN(tk.Frame):
"""
This is the second page, it contains a text box where you will list the
names of the vpn's that you want to edit. It will also display the
directories
obtained by the pullconfig script.
"""
def read_list(self):
vpn_list=str(self.var_vpn_list.get()).upper()
return vpn_list
def __init__(self, parent, controller, **kwargs):
self.controller = controller
tk.Frame.__init__(self, parent)
self.var_vpn_list = tk.StringVar()
label=tk.Label(self, text="Please list the VPNs desired to edit")
label.pack()
#Create text box to submit a list of vpns back to the main program
vpnLabel = tk.Label(self, text="VPN Names").pack()
self.TextBox = tk.Entry(self, textvariable=self.var_vpn_list)
self.TextBox.pack()
vpnListSubmit = tk.Button(self, text="Enter", command= lambda:
self.read_list() and self.controller.show("pickFiles"))
vpnListSubmit.pack()
class pickFiles(tk.Frame):
"""
Second page that allows you to select your desired files from the
edit directory specified in the config file. Check all desired files,
list will be returned to the program.
"""
def get_vpn_list(self):
list = self.controller.get_page("listVPN").var_vpn_list.get()
self.vpn_list = str(list).upper()
self.vpn_label.configure(text="VPN List: %s"%self.vpn_list)
return self.vpn_list
def __init__(self, parent, controller,**kwargs):
# Inherits from the tk.Frame class
tk.Frame.__init__(self, parent)
self.controller = controller
self.vpn_list = tk.StringVar()
list = self.controller.get_page("listVPN").var_vpn_list.get()
self.vpn_list = str(list).upper()
show_vpn = tk.Button(self, text="Show vpnlist", command =
self.get_vpn_list)
show_vpn.pack()
self.vpn_label = tk.Label(self, text="VPN List: %s" %self.vpn_list)
self.vpn_label.pack()
# todo: get external module function to run with variable input
#file_list = Rename.searchFile(vpnlist)
# Execute program on calling the parent class
if __name__=="__main__":
app = vpnRenameProgram()
app.mainloop()
EDIT:
Above is my whole code with custom scripts I've imported commented out. My main question is about layout. I want the frames to stack on top of one another, but they are not. Why is it doing this and what would get me on track to getting the layout I want?

The main problem with your code is that you're creating multiple containers. The code that served as a base for your program was specifically designed to have a single container with multiple frames within the container.
The first step is to create the container, and save a reference so it can be used later:
class vpnRenameProgram(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.container = tk.Frame(self)
self.container.pack(side="top", fill="both", expand=True)
self.container.grid_rowconfigure(0, weight=1)
self.container.grid_columnconfigure(0, weight=1)
self.frames = {}
self.show_frame("MainView")
I'm also going to recommend that you pass the actual class instead of a class name. This way you don't have to dig into globals() to try to find the right class based on the name.
Change the last line in the above to look like this:
self.show_frame(MainView)
You will also need to change get_page, but it's now a simple lookup:
def get_page(self, page_class):
return self.frames.get(page_class, None)
The final step is to redefine show to create the frame on demand. You've created a method called instantiate_page, but I see no real reason not to put it all in a single function since it's only a couple extra lines of code:
def show(self, page_class):
if page_class in self.frames:
frame = self.frames[page_class]
else
frame = page_class(parent=self.container, controller=self)
frame.grid(row=0, column=0, sticky="nsew")
self.frames[page_class] = frame
frame.tkraise()
That's all there is to it. You just need to remember to pass the class rather than the name of the class when calling show or get_page (eg: controller.show(listVPN), controller.get_page(pickFiles)`, etc)

Related

how to change the label property outside of the python Tkinter class

I have a tkinter project, the main script (not a full program, to long) looks like this, label (led_A) is created inside a TK class,
....
def fun_A:
...
#need to hidden led_A
#**need to implement** led_A.forget() or .place()
class home(tk.Tk)
def __init__(self, *args, **kwargs)
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.wm_title(self, "Graph ")
self.geometry("1000x800")
container = tk.Frame(self)
container.pack(side="top", fill="both", expand = True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
F=Graph
frame=Graph(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(Graph)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class Graph(tk.frame):
def __init__(self,parent,controller):
...
photo = PhotoImage(file='1.jpg')
led_A = label(self, image = photo)
app = home()
app.mainloop()
So I try to implement led_A.forget in fun_A, how could I do? I tried global led_A but main program could not recognize it. Thanks
To manipulate an object you need a reference to the object. In this specific case, you need to make led_A an instance attribute so that you can reference it. You also need to add a method in your controller to give you access to the instance of Graph.
To make led_A an instance attribute:
class Graph(tk.frame):
def __init__(self,parent,controller):
...
photo = PhotoImage(file='1.jpg')
self.led_A = label(self, image = photo)
...
To add a method to the controller, do something like this:
class home(tk.Tk)
...
def get_frame(self, frame_class):
return self.frames[frame_class]
Ideally, func_A shouldn't know anything about how Graph is implemented since func_A isn't inside Graph. That means that you need to add a function to Graph which will add or remove the led.
class Graph(tk.frame):
...
def turn_on_led(self):
self.led_A.grid(...)
Finally, you need to modify fun_A to use this method to get a reference to the graph and call the function:
def fun_A():
graph = app.get_frame(Graph)
graph.turn_on_led()

How to reference correctly to frame names in tkinter

I am creating a card game in tkinter and need help with referencing to the frame names. My problem is that when I want to "refresh" the frame, I need to destroy and recreate it and this changes the progressive numbering of the frames.
Please take a look at the code below. The example shows that the third frame every time gets a new name as it gets recreated.
import tkinter as tk
class RootFrame(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.main_window = tk.Frame(self)
self.main_window.pack(side="top", fill="both", expand=True)
self.main_label = tk.Label(self.main_window, text="Main Window")
self.main_label.pack()
self.second_frame = SecondFrame(self.main_window, self)
self.second_frame.pack()
class SecondFrame(tk.Frame):
def __init__(self, parent, controller, *args, **kwargs):
super().__init__(*args, **kwargs)
self.controller = controller
label = tk.Label(self, text="Second Frame")
label.pack()
self.create_third_frame()
def create_third_frame(self):
self.third_frame = ThirdFrame(self, self.controller)
self.third_frame.pack()
def update_frame(self):
self.third_frame.destroy()
self.create_third_frame()
class ThirdFrame(tk.Frame):
def __init__(self, parent, controller, *args, **kwargs):
super().__init__(*args, **kwargs)
self.controller = controller
self.parent = parent
label = tk.Label(self, text="Third Frame")
label.pack()
refresh_button = tk.Button(
self, text="Resfresh", command=self.parent.update_frame)
refresh_button.pack()
print(self.winfo_name())
if __name__ == "__main__":
app = RootFrame()
app.mainloop()
The code above is used to illustrate the problem. Please run the code and you'll see the changing widget name in the terminal.
I use winfo_parent and winfo_name in my actual code to create different conditions for button bindings. For example, if the user clicks a widget1 in frame6 happens X and when I click a widget8 in frame2 happens Y. This works until I destroy() and recreate something and everything breaks.
I suppose that using winfo_name and winfo_parent for this kind of referencing is not the correct way to get around, but I really can't think of anything else.
I'm not sure exactly what you are asking, but you can assign a specific name to the widget:
def create_third_frame(self):
self.third_frame = ThirdFrame(self, self.controller, name='testframe')
self.third_frame.pack()
Then each time the name of the frame created will be consistent.
You can also reference the widget by name with Tk().nametowidget(), see this relevant answer here: Is it possible to search widget by name in Tkinter?
>>> from Tkinter import *
>>> win = Tk()
>>> button = Button( Frame( win, name = "myframe" ), name = "mybutton" )
>>> win.nametowidget("myframe.mybutton")
<Tkinter.Button instance at 0x2550c68>
I would recommend sticking with a OOP approach however, and just reference it with from your code like self.thirdframes where you might have a list or dict of ThirdFrame objects. This way your python code can easily reference the objects without going back to the tcl interpreter and parse the widget name. If you ever will only have one ThirdFrame, then just reference back to self.thirdframe whenever you need it.

Understanding when to use classes vs methods in OOP - switching frames with tkinter

I'm trying to replicate an answer to my own accord from this answer/question here: Switch between two frames in tkinter
As you can see from the answer above, tehy instantiate multiple sub-classes of the Frame widget, and when we want to switch pages, we click a button and that class a method from our base class.
However, wouldn't creating multiple 'pages' methods under a 'Pages' class, be a lot cleaner and make sense? I'm not sure what to believe, and I would love clarification as to how I should be tackling this project, and why using classes or instanced methods would be better?
I've added my comments into the code below for lines I don't quite understand and I'm hoping I can gain some knowledge from the world of StackOverflow.
import Tkinter as tk
LARGE_FONT = ("Verdana", 12)
class Main(tk.Tk):
def __init__(self, *args, **kwargs):
########### are we calling the Tk class from tkinter and passing in our 'Main' class?
tk.Tk.__init__(self, *args, **kwargs)
# why not self.container?
container = tk.Frame(self)
# again, should I be using self.page here instead?
page = Pages(parent=container, controller=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 ('page.page_one()', 'page.page_two()'):
# what exactly does __name__ do? And how can I replicate this with my derived classes instanced methods?
page_name = F#F.__name__
frame = page#(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=0, column=0, sticky='nsew')
self.show_frame("page.page_one()")
def show_frame(self, page_name):
frame = self.frames[page_name]
frame.tkraise()
class Pages(tk.Frame):
# i could just use *args, **kwargs here couldn't I?
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.page_one(controller)
# here I have my instance methods inside my derived 'Pages' class
# isn't this much cleaner, having multiple instance methods inside a class?
# or I should be separating each page into it's own instanced class?
def page_one(self, controller):
label = tk.Label(self, text='show_firmware_page', font=LARGE_FONT)
label.pack(pady=10, padx=10)
# how can I switch pages using the next button?
next_btn = tk.Button(self, text='Next', command=lambda: controller.show_frame(self.page_two(controller)))
quit_btn = tk.Button(self, text='Quit', command=lambda: controller.show_frame())
quit_btn.pack()
next_btn.pack()
def page_two(self, controller):
label = tk.Label(self, text='second_page', font=LARGE_FONT)
label.pack(pady=10, padx=10)
# how can I switch pages using the next button?
next_btn = tk.Button(self, text='Next', command=lambda: Pages.show_frame("page_one"))
quit_btn = tk.Button(self, text='Quit', command=lambda: Pages.show_frame())
quit_btn.pack()
next_btn.pack()
app = Main()
app.mainloop()
Basically, my current push is to try and use methods within my class in order to define my pages and switch between them. I'm currently having some trouble, and upon taking a look at other's answers, a lot of them have the idea that each class instantiates a Frame, in which we call a method to switch between those instances.
Let me know your thoughts on this process to get me up to speed about how I should be tackling this project.
Many thanks in advance for your help - I really want to get this OOP stuff down.
You could theoretically make methods in a single class reconfigure the UI the way you want, but it's probably not going to be easy.
Your current code can't work because there's no simple way for one of your methods to clean up the work done by another previous method (e.g. by getting rid of the label and buttons it created).
The original answer you linked to avoided that issue by having all the widgets created at the start of the program (not only when they're about to be displayed). Only one page is displayed at a time though, since they're all Frames that have been configured to display in the same location. The Frame that is on top hides the others and prevents their widgets from doing anything. By moving a different frame on top, you can switch between the pages.
If we ignore those widget display issues, you could make your main class call the methods you've written:
class Main(tk.Tk):
def __init__(self, *args, **kwargs):
container = tk.Frame(self) # I'm not sure a separate container is necessary
container.pack(side="top", fill="both", expand=True) # since we're only putting
container.grid_rowconfigure(0, weight=1) # one other frame inside it
container.grid_columnconfigure(0, weight=1)
page = Pages(parent=container, controller=self)
page.grid(row=0, column=0, sticky='nsew') # moved up from below
self.frames = {}
for F in (page.page_one, page.page_two): # no quotes or parentheses
page_name = F.__name__ # the names are the strings "page_one" and "page_two"
self.frames[page_name] = F
self.show_frame("page_one")
def show_frame(self, page_name):
method = self.frames[page_name]
method() # call the method we are switching to
class Pages(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
def page_one(self):
# do something to clean up previous pages here?
label = tk.Label(self, text='show_firmware_page', font=LARGE_FONT)
label.pack(pady=10, padx=10)
next_btn = tk.Button(self, text='Next',
command=lambda: self.controller.show_frame("page_two")
next_btn.pack()
def page_two(self):
label = tk.Label(self, text='second_page', font=LARGE_FONT)
label.pack(pady=10, padx=10)
next_btn = tk.Button(self, text='Next',
command=lambda: self.controller.show_frame("page_one"))
next_btn.pack()
This will work (for some definition of "work"). I removed the quit buttons because I'm not sure exactly what the best way to handle them is (you probably don't want them to be calling show_frame).
Note that I'm by no means an expert at TKinter, so it's entirely possible that there's some easy way to remove the widgets from the previous page when you've moved on to the next one. I just don't know how.

Changing a tkinter frame from inside a method/function other than __init__

What im trying to do is find a way to change the Frame from a method/function other than def __init__
My main class which im using to change and manage the Frames with is:
class ShopCounter(tk.Tk):
def __init__ (self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for i in (PageOne, PageTwo):
frame = i(container, self)
self.frames[i] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.showFrame(Login)
def showFrame(self, cont):
frame = self.frames[cont]
frame.tkraise()
The issue im having from here is that when i make a new window such as
class PageOne (tk.Frame):
def __init__ (self, parent, controller):
tk.Frame.__init__(self, parent)
lbl = tk.Label(self, text="Page 1")
lbl.pack()
This is also in __init__
as this calls the nextPage function (Later shown)
nextPage()
But i need to read from a CSV spread sheet to check if the person has inputted the user name and pass word (Haven't put checking code or input fields for convenience) i then find that i need to change the frame from a different function in the class PageOne
I have then met the problem of now having to define a controller in this new function s through the parenthesis of the function when it is called which is a problem because i don't know how to do that apart from just putting it as a parameter in the __init__method
Quick example:
Say the function in the class is something like this..
def nextPage (self, controller):
controller.showFrame(PageTwo)
IDLE then tells me that i the arguments for this function "controller" is missing but i don't know how I'd set that manually
I have found a fix
class PageOne (tk.Frame):
def __init__ (self, parent, controller):
tk.Frame.__init__(self, parent)
lbl = tk.Label(self, text="Page 1")
lbl.pack()
button = tk.Button(self, text="switch", command=lambda: self.launch(controller)
button.pack()
def launch (self, cont):
cont.showFrame(PageTwo)
Now I've made a function (launch) in the PageOne class which takes self and cont (the controller) as the parameters. You will then pass the controller from init into the launch function when you press the button.
Hope this helps some on. 'Cause personally I've been stumped on this one for months.
UPDATE
After months of this answer being up, i have now found a new way, rather than always passing the controller as a parameter to each function you with to use in each page class, in the initiation of the class (__ init __), you can declare a variable to the object that holds the controller, heres an example:
class PageTwo(tk.Frame):
def __init__ (self, parent, controller):
self.controller = controller
def doSomething (self):
self.controller.doSomething()
by referencing the controller you can run any class within the controller class, you can also get variable data and change it.

Trouble binding events in TKinter

For background: most of my experience is ruby/rails. I'm trying to help a friend by building a simple GUI app that updates an Excel file and don't have much experience w/ Python or TKinter. The goal is to have a simple form, the user enters a number, and another form is shown with a drop down menu. I decided to store the given number in a global variable as I have had trouble trying to pass a variable between the two frames. I cannot manage to both set the global variable and switch to the second frame. Other questions/issues I've had are in ## marked comments.
Alternatively, if anyone has any ideas on the best way to make a cross platform app that can access an MDB or excel file, I'm all ears. It kind of blows me away how difficult this has been. Thanks for any help.
import Tkinter as tk
TITLE_FONT = ("Helvetica", 18, "bold")
ID_NUMBER = None
class StatusApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(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 (EntryPage, StatusPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(EntryPage)
def show_frame(self, c):
'''Show a frame for the given class'''
frame = self.frames[c]
frame.tkraise()
class EntryPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Enter ID:", font=TITLE_FONT)
self.entry = tk.Entry(self)
## Using the lambda works to switch frames, but I need to be able to execute
## multiple statements.
# entry.bind('<Return>', lambda event: controller.show_frame(StatusPage))
## In examples I've seen, callback has been used without the empty parens, not sure
## why they're needed?
self.entry.bind('<Return>', self.callback())
label.pack(side="top", fill="x", pady=10)
self.entry.pack()
self.entry.focus_set()
def callback(self):
## I noticed the following gets fired once the program starts
print 'hello'
## For some reason it says that entry doesn't have the attribute 'set'. I don't
## understand this as I'm calling it like a method.
self.entry.set('hello')
## Ultimately setting the global ID_NUMBER variable is one of the main goals of this
## function
ID_NUMBER = self.entry.get()
## I haven't been able to switch frames from within this function, only w/ a lambda as
## seen on line 34.
# show_frame(StatusPage())
class StatusPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="ID: ", font=TITLE_FONT)
optionList = ('train', 'plane', 'boat')
selected_opt = tk.StringVar()
selected_opt.set(ID_NUMBER)
menu = tk.OptionMenu(self, selected_opt, *optionList)
button = tk.Button(self, text="Save", command=lambda: controller.show_frame(EntryPage))
label.pack(side="top", fill="x", pady=10)
menu.pack()
button.pack()
if __name__ == "__main__":
app = StatusApp()
app.mainloop()
The object of a binding must be a reference to a callable function. Lambda is often used in this context because it creates an anonymous function and returns a reference).
When you do ...bind(..., self.callback()), you are calling that function at the time the bind statement executes. In the code you make the comment ## I noticed the following gets fired once the program starts; this is why. The result of this function call is what is associated with the binding. Quite often, and in your specific case, this is the value None. You must omit the ()
In the code comments you wrote
## For some reason it says that entry doesn't have the attribute 'set'. I don't
## understand this as I'm calling it like a method.
self.entry.set('hello')
What makes you believe an entry widget has a set method? No documentation that I know of makes that claim. The error message is correct, the entry widget has no attribute named "set" (functions are considered attributes in this context).

Categories