Trouble binding events in TKinter - python

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).

Related

How can I create multiple frames with a for loop on a Tkinter window?

Note I tried to put this in stack overflow with correct formatting, but even when I pressed the code formatting button, it didn't process the response. Can someone help me with that as well?. I am posting here without the formatting as I need help quite fast. I know many people on this forum are really good at programming, so I thought of reaching out.
I am working on an application using python, and more specifically, I'm using Tkinter for the user interface. I have over twenty new pages I need to make within the application, and I as a result, I thought of using a for loop and an outline class structure of which I would create new instances (as their own pages which would later be linked around the application using buttons) to be the classes I use. However, I continue to get the following error when I run my code:
File "setup.py", line 215, in <module>
pages_dict[info_req[info][filetype][0]] = outline_info(new_object)
TypeError: __init__() takes exactly 4 arguments (2 given)
I understand why this makes sense, as the init function definition contains 4 arguments, but I am not sure about how I can make the controller (which is defined in the initial application class) and the parent window part of the arguments for the instance of the outline_info class, as I cannot refer to self in those scenarios as the classes haven't even been declared or made up until that point of declaration as an instance (If this explanation seems confusing, please look at the code below for further clarification as well).
An excerpt of my code is shown below, addressing the above concerns. Please let me know if more information is needed to understand or clarify my problem. The code below does not include many other classes I have defined, as well as an array called info_req, which contains a database of information.
class Application(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
# 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)
self.frames = {}
for F in (PageOne, PageThree, PageFour, indpage, familypage, FinalPage):
page_name = F.__name__
frame = F(parent=container, controller=self)
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("PageOne")
def show_frame(self, page_name):
'''Show a frame for the given page name'''
frame = self.frames[page_name]
frame.tkraise()
class outline_info(tk.Frame):
def __init__(self, parent, controller, info_type_array):
count = 0 # Just for reference
tk.Frame.__init__(self, parent)
self.controller
first_title = Label(self, text=info_type_array[0]).grid(row=1,column=1)
for i in range(1, len(info_type_array)):
text_first = Label(self, text=str(info_type_array[i])).grid(row=d+1, column=1)
entry_first = Entry(self, width=20).grid(row=d+1, column=2)
count += 1
def submit_again():
controller.show_frame("indpage")
return "hello" # Do I still need this?
submit2 = Button(self, text="Submit Details", bg="blue", command=submit_again)
submit2.pack(side=BOTTOM)
pages_dict = {}
for i in range(0, len(info_req)):
for filetype in range(0, len(info_req[i])):
if filetype!=0:
new_object = info_req[i][filetype]
if info_req[i][filetype][0] not in pages_dict.keys():
pages_dict[info_req[i][filetype][0]] =outline_info(new_object)
Many thanks.
Edit:
The following is a snippet of info_req. I am creating a beginner's travel guide, but I really want to learn how to tackle the problem in the original post.
info_req = [ [ ["Places Visited"], ["Country 1", "City Visited", "Reccomendations"], ["Country 2", "City Visited", "Reccomendations"] ], [ ["Favorite Food"], ["Food 1", "Cuisine", "Taste", "Flavor"], ["Food 2", "Cuisine", "Taste", "Flavor"] ], [ ["Favorite Airlines"], ["Airline 1", "Organization", "Position", "Duration"] ] ]
Solution
As you want the controller to be accessible, you can make the outline_info class inherit the Application class. Then, call super().__init__() in the outline_info class and DO NOT instantiate the Application class but the outline_info class. Here is your code with that:
Code
class Application(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs)
# 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)
self.frames = {}
for F in (PageOne, PageThree, PageFour, indpage, familypage, FinalPage):
page_name = F.__name__
frame = F(parent=container, controller=self)
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("PageOne")
def show_frame(self, page_name):
'''Show a frame for the given page name'''
frame = self.frames[page_name]
frame.tkraise()
class outline_info(Application):
def __init__(self, parent, controller, info_type_array):
count = 0 # Just for reference
tk.Frame.__init__(self, parent)
self.controller
first_title = Label(self, text=info_type_array[0]).grid(row=1,column=1)
for i in range(1, len(info_type_array)):
text_first = Label(self, text=str(info_type_array[i])).grid(row=d+1, column=1)
entry_first = Entry(self, width=20).grid(row=d+1, column=2)
count += 1
def submit_again():
controller.show_frame("indpage")
return "hello" # Do I still need this?
submit2 = Button(self, text="Submit Details", bg="blue", command=submit_again)
submit2.pack(side=BOTTOM)
pages_dict = {}
for i in range(0, len(info_req)):
for filetype in range(0, len(info_req[i])):
if filetype!=0:
new_object = info_req[i][filetype]
if info_req[i][filetype][0] not in pages_dict.keys():
pages_dict[info_req[i][filetype][0]]=outline_info(new_object)
There are some formatting errors which hopefully you can fix! Use the same indentation everywhere (Some places 8 space and some places 4 spaces you have used).
Suggestions and Other Information
You have used tk.wdg in some places and directly wdg in some cases. Note in the code I have posted, I haven't posted a working example but how the soultion works since the source code contains much more code than you have posted thus making the flow unclear. If you want anything to be usable in the Application class, just use it with self since self is actually an object of submit_info passed on to Application class. Hope this helped!

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.

tkraise not hiding 'bottom' frame

I'm struggling with tkraise not hiding the 'bottom' frame in my app.
I have two frames, one contains a Listbox and is packed to the left and the other will display options for each item in the listbox and is packed to the right.
My problem is that I can see the Future page when I select General and vise versa. I copied and modified it from my working main app but I don't know what I did wrong to break it for this one.
# All settings windows and forms labels are built in here
import tkinter as tk
# from main import WinSize
from tkinter import Listbox, END, ttk
class Settings(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
# create frame for listbox and parameters area
self.list_container = ttk.Frame(self, relief='sunken')
self.list_container.pack(side='left', fill='y', expand=False)
self.param_container = ttk.Frame(self)
self.param_container.pack(side='top', fill='both', expand=True)
self.options_list = Listbox(self.list_container, selectmode='single')
for choice in ['General', 'Future']:
self.options_list.insert(END, choice)
self.okbutton = ttk.Button(self.param_container, text="OK", command= self.destroy)
self.okbutton.grid_rowconfigure(0, weight=1)
self.okbutton.grid_columnconfigure(0, weight=1)
self.okbutton.grid(row=2, column=2, sticky='nsew')
# Grid layout for Settings window
self.options_list.grid(row=0, column=0, sticky='nsew')
self.list_container.grid_rowconfigure(1, weight=1)
self.list_container.grid_columnconfigure(1, weight=1)
self.param_container.grid_rowconfigure(1, weight=1)
self.param_container.grid_columnconfigure(1, weight=1)
# create empty TUPLE for future frames
self.frames = {}
# generate calls for frames
for F in (General, Future):
self.page_name = F.__name__
self.frame = F(parent=self.param_container, controller=self)
self.frames[self.page_name] = self.frame
self.options_list.select_set(0)
self.options_list.bind("<<ListboxSelect>>", self.onselect)
self.options_list.event_generate("<<ListboxSelect>>")
# grab value of listbox selection and call show_frame
def onselect(self, event):
self.widget = event.widget
self.value = self.widget.get(self.widget.curselection())
print(self.value)
self.show_frame(self.value)
# show corresponding frame based on listbox selection
def show_frame(self, page_name):
# show a frame for the given page name
self.frame = self.frames[page_name]
self.frame.grid(row=0, column=1, sticky='nsew')
self.frame.tkraise()
print("Show Frame")
class General(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.optiontitle = ttk.Label(parent, text='General')
self.optiontitle.grid(row=0, column=0, sticky='nsew')
self.dirlabel = ttk.Label(parent, text='Default Save Directory')
self.dirlabel.grid(row=1, column=1, sticky='s')
class Future(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
test1 = ttk.Label(self, text='Future')
test1.pack()
app=Settings()
app.mainloop()
I want to say it may be something to do with my grid layout but it doesn't make sense since the two 'pages' are not coupled (or supposed to be) with each other.
I solved this issue through another problem that I was able to work out with some help from others. Refer to this → Frames not stacking on top of each other in tkinter question as it has two great answers which easily allow to incorporate grid_forget() and/or pack_forget() as suggested by #jasonharper.
You aren't doing anything to actually hide the other page; you're just layering the new page on top of it, which isn't going to look right unless they occupy exactly the same screen area.
Even if they did, this still isn't a workable approach, since the widgets in the hidden page are still active. In particular, if it had any Entry or Text fields, they could still have (or gain) keyboard focus, so anything the user types might mysteriously end up in a field they can't even see at the moment.
You should call .grid_forget() on the previous page when showing the new one. Or, perhaps easier, call .grid_forget() on all pages before calling .grid() on the new one (it doesn't hurt to call this on a widget that isn't currently shown).

Dynamically instantiate pages Tkinter

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)

Double-lambda Python

Is it feasible to execute multiple statements in a lambda? The app_data dictionary in my controller has a property "listbox" that stores a selected listbox value. A button on SelectPage has a lambda command that changes frames to NextPage.
Can the frame switch and app_data set statements both occur in a double-lambda? If so, what might this look like? Or do these operations have to occur separately (i.e. in a button click event)?
Update #1:
Issue isolated to lambda expression since just putting a constant (i.e. 1) in like .set(1) does not save either.
Update #2:
Using a lambda with doStuff(controller) solves the problem somewhat, but the listbox reference throws an error:
value = self.listbox.get(self.listbox.curselection())
AttributeError: 'SelectPage' object has no attribute 'listbox'
button:
button1 = ttk.Button(self, text='Next Page',
command=lambda:self.doStuff(controller))
doStuff():
def doStuff(self,controller):
controller.show_frame(NextPage)
controller.app_data["listbox"].set(1)
value = self.listbox.get(self.listbox.curselection())
print(value)
print("do Stuff successful")
Full Code:
from tkinter import *
from tkinter import ttk
class MyApp(Tk):
# Controller class
def __init__(self):
Tk.__init__(self)
# App data in controller
self.app_data = {"listbox": StringVar(),
"entry": StringVar(),
}
container = ttk.Frame(self)
container.pack(side="top", fill="both", expand = True)
self.frames = {}
for F in (SelectPage, NextPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky = NSEW)
self.show_frame(SelectPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
def get_page(self,classname):
for page in self.frames.values():
if str(page.__class__.__name__) == classname:
return page
return None
class SelectPage(ttk.Frame):
def __init__(self, parent, controller):
self.controller = controller
ttk.Frame.__init__(self, parent)
ttk.Label(self, text='Select Page').grid(padx=(20,20), pady=(20,20))
listbox = Listbox(self,exportselection=0)
listbox.grid()
for item in [0,1,2,3,4,5]:
listbox.insert(END, item)
print (item)
entry1 = ttk.Entry(self, textvariable=self.controller.app_data["entry"], width=8)
entry1.grid()
button1 = ttk.Button(self, text='Next Page',
command=lambda:self.doStuff(controller)) # something like this lambda concept
button1.grid()
def doStuff(self,controller):
controller.show_frame(NextPage)
controller.app_data["listbox"].set(1)
value = self.listbox.get(self.listbox.curselection())
print(value)
print("do Stuff successful")
class NextPage(ttk.Frame):
def __init__(self, parent, controller):
self.controller = controller
ttk.Frame.__init__(self, parent)
ttk.Label(self, text='Next Page').grid(padx=(20,20), pady=(20,20))
button1 = ttk.Button(self, text='Select Page',
command=lambda: controller.show_frame(SelectPage))
button1.grid()
button2 = ttk.Button(self, text='press to print', command=self.print_it)
button2.grid()
def print_it(self):
value = self.controller.app_data["listbox"].get()
print ('The value stored in StartPage some_entry = ' + str(value))
value = self.controller.app_data["entry"].get()
print ('The value stored in StartPage some_entry = ' + str(value))
app = MyApp()
app.title('Multi-Page Test App')
app.mainloop()
Your original lambda didn't work because show_frame(...) returns None, which short-circuits the and so the remaining expression doesn't execute. Just change it to an or. Since the event caller doesn't care what you return, you could also create a two item tuple for the two subexpressions. There are other problems, such as self.listbox doesn't exist, which I fixed, but others remain.
As a pedantic aside, lambda only accepts an expression, which is a subset of a statement.
self.listbox = listbox
button1 = ttk.Button(self, text='Next Page',
command=lambda: controller.show_frame(NextPage) or self.controller
.app_data["listbox"]
.set(self.listbox.get(self.listbox.curselection()))) # something like this lam
Is it feasible to execute multiple statements in a lambda?
No. In general, you should avoid using lambda at all, and when you must use it you need to keep it as brief as possible. If you need to call more than a single function, create a new function that calls the other functions.
Also, if you're passing in an argument that is just an attribute of the object, you don't need lambda at all. I think the following code is much easier to read, much easier to understand, much easier to write, and thus much easier to maintain:
button1 = ttk.Button(self, text='Next Page',command=self.doStuff)
...
def doStuff(self):
self.controller.show_frame(NextPage)
self.controller.app_data["listbox"].set(1)

Categories