I'm currently working on a tkinter project. The code is shown below:
import tkinter as tk
from tkinter import font as tkfont
background_colour = "#F0F0F0"
class Application(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title("CCNA Command Learner")
self.geometry("500x500")
self.iconbitmap("favicon.ico")
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 (IntroPage, MainPage):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("IntroPage")
def show_frame(self, page_name):
frame = self.frames[page_name]
frame.tkraise()
class IntroPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.image = tk.PhotoImage(file="IntroImage.gif")
self.head_img = tk.Label(self, image=self.image, background=background_colour)
self.head_img.pack()
self.label = tk.Label(self, bg=background_colour,
text="\nLearn essential commands for the CCNA exam\n\nPlease enter your name")
self.label.pack()
self.head_entry = tk.Entry(self)
self.head_entry.pack()
self.submit = tk.Button(self, text="Submit", command=self.submit_button)
self.submit.pack()
def submit_button(self):
global var_username
self.controller.show_frame("MainPage")
var_username = self.head_entry.get()
class MainPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.introduction = tk.Label(self, text="Hello" + var_username)
self.introduction.pack()
if __name__ == "__main__":
app = Application()
app.mainloop()
My problem is that in the submit_button method of my IntroPage class, I am trying to set a global variable so that my MainPage class can then access it. However, despite declaring it and setting it, I am still getting a NameError when accessing it in my MainPage class?
That's correct, as you seem to use global incorrectly. It can make existing variable out of your scope visible and available for modification, like this:
a = 'test'
def f():
global a
a = 'edited'
f()
print(a)
Output:
>>> edited
You don't seem to declare variable with this name, hence the NameError. And again - global is not for creating global variables, it is for making them visible.
By the way, think twice before using globals in python, that usually indicates huge design problems. It's surely possible to refactor your code in a way that you won't need global.
You simply need create it before MainPage in call stack.
Create it outside the IntroPage class, and initializing can stay where it is.
Related
Pardon me for my bad grammar or explanation, since I didn't know how to explain this properly.
I try to build some gui that could switch between frame, using script from this as base Switch between two frames in tkinter.
In this case, I will have a few frame that had similar design, but different function when the button is pressed. For example, I have 2 frames that have similar 2 entries and 1 button, but the button do different command (where at sub01 frame it will multiply and at sub02 frame will divide)
This is my code:
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
container.grid(row=1,columnspan=4,sticky='nsew')
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (sub01, sub02):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=1,sticky="nsew")
self.choices = {'sub01','sub02'}
self.tkvar = tk.StringVar()
self.tkvar.set('sub01')
self.popMenu = tk.OptionMenu(self,self.tkvar,*self.choices)
self.popMenu.grid(row=0)
self.show_frame()
self.button1 = tk.Button(self, text="Go to Layer",command=lambda: self.show_frame())
self.button1.grid(row=0, column=1)
def show_frame(self):
'''Show a frame for the given page name'''
page_name = self.tkvar.get()
frame = self.frames[page_name]
frame.tkraise()
class sub01(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This SubLayer 1")
label.grid(row=0)
self.entries=[]
i = 0
while i < 2:
self.entries.append(tk.Entry(self,width=10))
self.entries[i].grid(row=i+1,columnspan=2,sticky='we')
i += 1
self.btn = tk.Button(self,text="multiply", command=lambda : self.multiply())
self.btn.grid(row=i+1, columnspan=2,sticky='we')
def multiply(self):
pass
class sub02(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This SubLayer 2")
label.grid(row=0)
self.entries=[]
i = 0
while i < 2:
self.entries.append(tk.Entry(self,width=10))
self.entries[i].grid(row=i+1,columnspan=2,sticky='w')
i += 1
self.btn = tk.Button(self,text="divide",command=lambda : self.divide())
self.btn.grid(row=i+1, columnspan=2,sticky='we')
def divide(self):
pass
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
This code itself works, but when I need to create more of these frames, it becomes inconvenient. How could I make this code simpler? Like having that similar frame as a class, and the button as other class that do differ behaviour depend of the layer shown.
Thank you in advance
The canonical way to do this sort of thing is to create a class hierarchy for your Page classes and put common functionality in the base classes and derive subclasses from them that specify the behavior that differs between them. Below is how you could do that with the sample code in your question.
Since the things that are different between them are:
The text displayed on the Label.
The text displayed on the Button.
The code in that's execute when the Button is clicked.
This means the derived classes only need to know what code to run in a generically named btn_func() method and what the text to displayed on the two widgets. The code below illustrates how to do that.
Note that I've changed the spelling of your class names to conform to the naming conventions describe in PEP 8 - Style Guide for Python Code.
import Tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
container.grid(row=1,columnspan=4,sticky='nsew')
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (Sub01, Sub02):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=1,sticky="nsew")
self.choices = {'Sub01','Sub02'}
self.tkvar = tk.StringVar()
self.tkvar.set('Sub01')
self.popMenu = tk.OptionMenu(self,self.tkvar,*self.choices)
self.popMenu.grid(row=0)
self.show_frame()
self.button1 = tk.Button(self, text="Go to Layer",command=lambda: self.show_frame())
self.button1.grid(row=0, column=1)
def show_frame(self):
'''Show a frame for the given page name'''
page_name = self.tkvar.get()
frame = self.frames[page_name]
frame.tkraise()
class BaseSubLayer(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text=self.lbl_text)
label.grid(row=0)
self.entries=[]
i = 0
while i < 2:
self.entries.append(tk.Entry(self,width=10))
self.entries[i].grid(row=i+1,columnspan=2,sticky='we')
i += 1
self.btn = tk.Button(self,text=self.btn_func_name, command=self.btn_func)
self.btn.grid(row=i+1, columnspan=2,sticky='we')
def btn_func(self):
raise NotImplementedError
class Sub01(BaseSubLayer):
lbl_text = 'This SubLayer 1'
btn_func_name = 'multiply'
def btn_func(self):
print('Running multiply() method.')
class Sub02(BaseSubLayer):
lbl_text = 'This SubLayer 2'
btn_func_name = 'divide'
def btn_func(self):
print('Running divide() method.')
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
This is a shortened example of a longer application where I have multiple pages of widgets collecting information input by the user. The MyApp instantiates each page as a class. In the example, PageTwo would like to print the value of the StringVar which stores the data from an Entry widget in PageOne.
How do I do that? Every attempt I've tried ends up with one exception or another.
from tkinter import *
from tkinter import ttk
class MyApp(Tk):
def __init__(self):
Tk.__init__(self)
container = ttk.Frame(self)
container.pack(side="top", fill="both", expand = True)
self.frames = {}
for F in (PageOne, PageTwo):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky = NSEW)
self.show_frame(PageOne)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class PageOne(ttk.Frame):
def __init__(self, parent, controller):
ttk.Frame.__init__(self, parent)
ttk.Label(self, text='PageOne').grid(padx=(20,20), pady=(20,20))
self.make_widget(controller)
def make_widget(self, controller):
self.some_input = StringVar
self.some_entry = ttk.Entry(self, textvariable=self.some_input, width=8)
self.some_entry.grid()
button1 = ttk.Button(self, text='Next Page',
command=lambda: controller.show_frame(PageTwo))
button1.grid()
class PageTwo(ttk.Frame):
def __init__(self, parent, controller):
ttk.Frame.__init__(self, parent)
ttk.Label(self, text='PageTwo').grid(padx=(20,20), pady=(20,20))
button1 = ttk.Button(self, text='Previous Page',
command=lambda: controller.show_frame(PageOne))
button1.grid()
button2 = ttk.Button(self, text='press to print', command=self.print_it)
button2.grid()
def print_it(self):
print ('The value stored in StartPage some_entry = ')#What do I put here
#to print the value of some_input from PageOne
app = MyApp()
app.title('Multi-Page Test App')
app.mainloop()
Leveraging your controller
Given that you already have the concept of a controller in place (even though you aren't using it), you can use it to communicate between pages. The first step is to save a reference to the controller in each page:
class PageOne(ttk.Frame):
def __init__(self, parent, controller):
self.controller = controller
...
class PageTwo(ttk.Frame):
def __init__(self, parent, controller):
self.controller = controller
...
Next, add a method to the controller which will return a page when given the class name or some other identifying attribute. In your case, since your pages don't have any internal name, you can just use the class name:
class MyApp(Tk):
...
def get_page(self, classname):
'''Returns an instance of a page given it's class name as a string'''
for page in self.frames.values():
if str(page.__class__.__name__) == classname:
return page
return None
note: the above implementation is based on the code in the question. The code in the question has it's origin in another answer here on stackoverflow. This code differs from the original code slightly in how it manages the pages in the controller. This uses the class reference as a key, the original answer uses the class name.
With that in place, any page can get a reference to any other page by calling that function. Then, with a reference to the page, you can access the public members of that page:
class PageTwo(ttk.Frame):
...
def print_it(self):
page_one = self.controller.get_page("PageOne")
value = page_one.some_entry.get()
print ('The value stored in StartPage some_entry = %s' % value)
Storing data in the controller
Directly accessing one page from another is not the only solution. The downside is that your pages are tightly coupled. It would be hard to make a change in one page without having to also make a corresponding change in one or more other classes.
If your pages all are designed to work together to define a single set of data, it might be wise to have that data stored in the controller, so that any given page does not need to know the internal design of the other pages. The pages are free to implement the widgets however they want, without worrying about which other pages might access those widgets.
You could, for example, have a dictionary (or database) in the controller, and each page is responsible for updating that dictionary with it's subset of data. Then, at any time you can just ask the controller for the data. In effect, the page is signing a contract, promising to keep it's subset of the global data up to date with what is in the GUI. As long as you maintain the contract, you can do whatever you want in the implementation of the page.
To do that, the controller would create the data structure before creating the pages. Since we're using tkinter, that data structure could be made up of instances of StringVar or any of the other *Var classes. It doesn't have to be, but it's convenient and easy in this simple example:
class MyApp(Tk):
def __init__(self):
...
self.app_data = {"name": StringVar(),
"address": StringVar(),
...
}
Next, you modify each page to reference the controller when creating the widgets:
class PageOne(ttk.Frame):
def __init__(self, parent, controller):
self.controller=controller
...
self.some_entry = ttk.Entry(self,
textvariable=self.controller.app_data["name"], ...)
Finally, you then access the data from the controller rather than from the page. You can throw away get_page, and print the value like this:
def print_it(self):
value = self.controller.app_data["address"].get()
...
I faced a challenge in knowing where to place the print_it function.
i added the following to make it work though I don't really understand why they are used.
def show_frame(self,page_name):
...
frame.update()
frame.event_generate("<<show_frame>>")
and added the show_frame.bind
class PageTwo(tk.Frame):
def __init__(....):
....
self.bind("<<show_frame>>", self.print_it)
...
def print_it(self,event):
...
Without the above additions, when the mainloop is executed,
Page_Two[frame[print_it()]]
the print_it function executes before PageTwo is made Visible.
try:
import tkinter as tk # python3
from tkinter import font as tkfont
except ImportError:
import Tkinter as tk #python2
import tkFont as tkfont
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")
# data Dictionary
self.app_data = {"name": tk.StringVar(),
"address": tk.StringVar()}
# 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 (StartPage, PageOne, PageTwo):
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("StartPage")
def show_frame(self, page_name):
''' Show a frame for the given page name '''
frame = self.frames[page_name]
frame.tkraise()
frame.update()
frame.event_generate("<<show_frame>>")
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="this is the start page", font=self.controller.title_font)
label.pack(side="top", fill="x", pady=10)
# Update the Name value only
self.entry1 = tk.Entry(self,text="Entry", textvariable=self.controller.app_data["name"])
self.entry1.pack()
button1 = tk.Button(self, text="go to page one", command = lambda: self.controller.show_frame("PageOne")).pack()
button2 = tk.Button(self, text="Go to page Two", command = lambda: self.controller.show_frame("PageTwo")).pack()
class PageOne(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is page 1", font=self.controller.title_font)
label.pack(side="top", fill="x", pady=10)
# Update the Address value only
self.entry1 = tk.Entry(self,text="Entry", textvariable=self.controller.app_data["address"])
self.entry1.pack()
button = tk.Button(self, text="Go to the start page", command=lambda: self.controller.show_frame("StartPage"))
button.pack()
class PageTwo(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
# Bind the print_it() function to this Frame so that when the Frame becomes visible print_it() is called.
self.bind("<<show_frame>>", self.print_it)
label = tk.Label(self, text="This is page 2", font=self.controller.title_font)
label.pack(side="top", fill="x", pady=10)
button = tk.Button(self, text="Go to the start page",
command=lambda: self.controller.show_frame("StartPage"))
button.pack()
def print_it(self,event):
StartPage_value = self.controller.app_data["name"].get()
print(f"The value set from StartPage is {StartPage_value}")
PageOne_value= self.controller.app_data["address"].get()
print(f"The value set from StartPage is {PageOne_value}")
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
I am making a game with levels and in each level, I will need to be using different operators and/or different ranges. My problem is that I don't know how to change the variables in a function from a different class. I would like to do this so I don't need to copy and paste my code making it lengthy. I'd like to use self.Answer and self.strQuestion for mulitple scope.
The code below is just to make the classes functional.
from tkinter import *
import tkinter as tk
import random
from Tkinter import messagebox
class BattleMaths(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 (StartPage, levelone, leveltwo):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(StartPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
lvl1_button = Button(self, text="LEVEL 1", command=lambda: controller.show_frame(levelone))
lvl1_button.place(relx=0.5, rely=0.5, anchor='center')
I want to put the questions def into class leveltwo while changing it to self.Answer = int(numOne) * int(numTwo) and self.strQuestion = "{} x {}".format(str(numOne), str(numTwo))
class levelone(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
def widgets(self):
#widgets here
def question(self):
self.UserAnswer = ''
numOne = random.randrange(1,10)
numTwo = random.randrange(1,10)
self.Answer = int(numOne) + int(numTwo) #change this
self.strQuestion = "{} + {}".format(str(numOne), str(numTwo)) #and change this
def answer(self):
#answer checker
class leveltwo(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
#question def here
root = BattleMaths()
root.title("Battle Maths")
root.geometry("400x250")
root.resizable(0,0)
root.mainloop()
Create the variables you want in the main class (BattleMaths), then you can alter them in the child classes via controller.my_variable.
Example: self.Answer created in BattleMaths and accessed in levelone via controller.Answer
I am trying to access the Text widget defined in class FirstPage from outside of the class.
I tried to solve this problem by creating a new instance of FirstPage, but could not find the right arguments to use. Also tried to use instance of GUI to gain the access, but unsuccessfully.
My problem is solved when I can use text.insert(0.0, t) from outside of the classes. It would help me modify the text displayed with Tkinter by functions that are not directly related with the GUI.
The origin of the code I am trying to use is found: Switch between two frames in tkinter
Also I removed lines that were not necessary for this question..
import Tkinter as tk
class GUI(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.geometry(self, '580x410')
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 = {}
frame = FirstPage(container, self)
self.frames[FirstPage] = frame
frame.grid(row=0, column=0, sticky="nsew")
frame = self.frames[FirstPage]
frame.tkraise()
class FirstPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
text = tk.Text(self , height=25, width=80)
text.grid(column=0, row=0, sticky="nw")
app = GUI()
app.mainloop()
EDIT:
Here is the working code:
import Tkinter as tk
class GUI(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.geometry(self, '580x410')
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 = {}
frame = FirstPage(container, self)
self.frames[FirstPage] = frame
frame.grid(row=0, column=0, sticky="nsew")
frame = self.frames[FirstPage]
frame.tkraise()
page_name = FirstPage.__name__
self.frames[page_name] = frame
def get_page(self, page_name):
return self.frames[page_name]
class FirstPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.text = tk.Text(self , height=25, width=80)
self.text.grid(column=0, row=0, sticky="nw")
app = GUI()
app.get_page("FirstPage").text.insert("1.0", "Hello, world")
app.mainloop()
There's nothing special you need to do. As with any python object, you simply need a reference to the object in order to manipulate it.
The concept in the code you started with is to have a "controller" that controls access to all of the pages, since that object is where the pages are created. You can add a function in the controller that gives you a reference to a page, and then you can use that to call a function on that page.
Here's the changes you need to make to the controller:
class GUI(tk.Tk):
def __init__(self, *args, **kwargs):
...
page_name = FirstPage.__name__
self.frames[page_name] = frame
...
def get_page(self, page_name):
return self.frames[page_name]
You also need to modify FirstPage to keep a reference to the widget so that you can access it later:
class FirstPage(tk.Frame):
def __init__(self, parent, controller):
...
self.text = tk.Text(...)
...
From within any other code you can now access the text widget via get_page (but your pages must save a reference to the controller for this to work).
class AnotherPage(tk.Frame):
def __init__(self, parent, controller):
...
self.controller = controller
...
def some_function(self):
...
first_page = self.controller.get_page("FirstPage")
text = first_page.text.get("1.0", "end-1c")
...
first_page.text.insert("end", "some new text\n")
Note that this technique works outside of any GUI pages. In your code, app is the controller, so you can do something like this:
app = GUI()
app.get_page("FirstPage").text.insert("1.0", "Hello, world")
In line 11 I tried to use the grid(), but it keeps giving me
"wm_grid() got an unexpected keyword argument 'row'" error
Can anyone take a look at this?
import tkinter as tk
class MainPage(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)
# pass frames as dictionaries
self.frames = {}
frame = StartPage(container, self)
self.frames[StartPage] = frame
# problem here****************************************************
frame.grid(row=1, column=1)
# problem here****************************************************
self.show_frame(StartPage)
def show_frame(self, controller):
frame = self.frames[controller]
frame.tkraise()
class StartPage(tk.Tk):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="StartPage")
label.pack(pady=10, padx=10)
root = MainPage()
root.mainloop()
I guess the issue for you comes because you are subclassing StartPage from tk.Tk , you cannot use row/column keyword arguments for grid in that case.
But I believe that you really did not intend it to be a subclass of tk.Tk , since you seem to be trying to initialize tk.Frame inside it. I believe the fix in your case would be to inherit the class from tk.Frame instead as -
class StartPage(tk.Frame):