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

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.

Related

Python - accessing a local variable from another class

I have tried to condense the code down as much as possible to make it clear what I am asking...
I have a variable called chosen_name, determined in a class called booking_frame, that I would like to access in the calendar_frame class.
Therefore, it would be obvious for calendar_frame to inherit the attributes of booking_frame - however, I believe (I'm probably completely wrong lol) that calendar_frame has to inherit the characteristics of Frame so that the whole program functions correctly.
The reason that calendar_frame is a completely separate class is so that it can appear as a different frame.
Extremely grateful for any help given :)
# import tkinter modules
from tkinter import *
from tkinter import ttk
import tkinter.font as tkFont
from PIL import ImageTk, Image
from tkcalendar import *
# define self
class tkinterApp(Tk):
def __init__(self,*args, **kwargs):
Tk.__init__(self, *args, **kwargs)
# creating a container
container = Frame(self)
container.pack(side = "top", fill = "both", expand = True)
container.grid_rowconfigure(0, weight = 1)
container.grid_columnconfigure(0, weight = 1)
# initialising frames to an empty array
self.frames = {}
menu_bar = Menu(container)
main_menu = Menu(menu_bar)
menu_bar.add_cascade(label="Main Menu", menu=main_menu)
main_menu.add_command(label="Welcome page", command=lambda: self.show_frame(welcome_frame))
main_menu.add_command(label="Book a vehicle", command=lambda: self.show_frame(booking_frame))
main_menu.add_command(label="Register as new user", command=lambda: self.show_frame(register_frame))
Tk.config(self, menu=menu_bar)
for F in (welcome_frame, booking_frame, register_frame, calendar_frame):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row = 0, column = 0, sticky = "nsew")
self.show_frame(welcome_frame)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class welcome_frame(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
welcome = Label(self, text="Hello, please use the menu above to navigate the interface")
welcome.grid(row=0, column=4, padx=10, pady=10)
class register_frame(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
register_label = Label(self, text="New user - enter your details below to use the Collyer's car park.")
register_label.grid()
class booking_frame(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
chosen_name = "Steve"
class calendar_frame(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
print(booking_frame.chosen_name)
app = tkinterApp()
app.geometry("1000x800")
app.title("Collyer's Car Park")
app.mainloop()
First you need to change local variable chosen_name to instance variable self.chosen_name inside booking_frame class, otherwise it cannot be accessed outside the class:
class booking_frame(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.chosen_name = "Steve" # changed to instance variable
Then you can access it via controller.frames[booking_frame].chosen_name inside calendar_frame class:
class calendar_frame(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
print(controller.frames[booking_frame].chosen_name)
Inheritance is to model relationships of objects which behave the same ("IS A" relationship). It is not meant to share data between objects.
A possible solution to your problem is to use a third object, that would be shared
between booking_frame and calendar_frame.
This object can be a Python dictionary for example ; you can pass it to all your
"frame" objects, or you can maybe decide to have it global (not very academic, for sure, but quick and dirty):
GLOBAL_STATE = {}
class booking_frame(Frame):
...
GLOBAL_STATE["chosen_name"] = "Steve"
class calendar_frame(Frame):
...
print(GLOBAL_STATE.get("chosen_name"))
I hope you can see now how you can refactor your code to share data between those objects.

Tkinter have stacking frames with list and pages in different modules

I have code below originally taken from the link here i have read all documentation in there but i feel im overlooking something, i have pages in separate modules as well as a separate module with a Add class for adding pages to the window.
The idea is to later be able to drop a module in a sub folder with a Pagexxx object within it and call the add page class to allow Tkinter to display it but i cannot seem to get the frames to stack.
Nav.py
import tkinter as tk
from Page import PageList
import Mypages
class Windowhandler(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)
PageList("Page1", container, self, Mypages.PageOne)
PageList("Page2", container, self, Mypages.PageTwo)
self.show_frame("Page1")
def show_frame(self, cont):
frameref = PageList.frames[cont]
print(frameref)
frameref.tkraise()
app = Windowhandler()
app.mainloop()
Page.py
class PageList():
frames = {}
def __init__(self, name, parent, cont, ref):
self.frames[name] = ref(parent=parent, controller=cont)
Mypages.py
import tkinter as tk
class PageOne(tk.Frame):
def __init__(self, parent, controller):
this = tk.Frame.__init__(self, parent)
label = tk.Label(this, text="Welcome to Page 1")
label.pack(pady=10, padx=10)
button1 = tk.Button(this, text="Back to Home",
command=lambda: controller.show_frame("Page2"))
button1.pack()
class PageTwo(tk.Frame):
def __init__(self, parent, controller):
this = tk.Frame.__init__(self, parent)
label = tk.Label(this, text="Welcome to Page 2")
label.pack(pady=10, padx=10)
button1 = tk.Button(this, text="Back to Home",
command=lambda: controller.show_frame("Page1"))
button1.pack()
Before asking for help from others, the first step is to validate your assumptions. You're assuming that tk.Frame.__init__(self, parent) returns something useful, but you never validated that assumption by checking to see if it is what you think it is.
The first problem is illustrated by two lines, which are essentially the same in both pages:
this = tk.Frame.__init__(self, parent)
label = tk.Label(this, text="Welcome to Page 1")
I'm guessing you were assuming that this would be set to the instance of the Frame. However, the __init__ function returns None so this is set to None. When None is passed as the parent of a widget, that widget becomes a child of the root window.
The solution is to not use this. Use self:
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Welcome to Page 2")
The second problem is that you never add the page to the container with grid, pack, or place, so they will never be visible.
You need to change Add to actually add the page to the container:
def Add(name, parent, cont, ref):
PageList.frames[name] = ref(parent=parent, controller=cont)
PageList.frames[name].grid(row=0, column=0, sticky="nsew")

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.

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)

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