Different frames displayed on one frame - python

I want to create a multiple page app but have troubles with raising different frames above another. I have followed sentdex's tutorials on this and tried a lot of different approaches but have a problem with all frame widgets being displayed on the same frame, not on different frames as I wanted.
The button and "Start page text" should be on the first frame and once I press the button I should invoke another frame to be raised above this one. The second frame's text ("Question page text") should let you know you are on another frame.
This does not happen. Both texts as well as the button are displayed on the same frame.
You can run the code bellow to see what I mean.
from tkinter import *
from tkinter import ttk
LARGE_FONT = ("Verdana", 12)
class Quiz(Tk):
frames = []
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
container = Frame(self, height=50, width=90)
self.frames.append(container)
container.pack()
self.show_frame(StartPage(container, self))
def show_subject(self):
container = self.frames[0]
self.show_frame(self, Question(container, self))
def show_frame(self, frame):
frame.tkraise()
class StartPage(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
text = Text(width=40, height=2, spacing1=15)
text.insert(INSERT, "Start page text")
text.pack()
button = ttk.Button(text="Geo", command=Quiz.show_subject(Quiz)).pack(fill=X)
class Question(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
text = Text(width=40, height=2, spacing1=15)
text.insert(INSERT, "Question page text")
text.pack()
app = Quiz()
app.mainloop()
Any suggestions would be very appreciated because I have been trying to solve this problem for quite some time now.

One of the cornerstones of the design you are copying is that everything within a page should be a child of that page. That's the whole point -- each "page" is treated as a self-contained widget. You aren't doing that.
If you want something to appear on a specific page, make it part of the page. If it's supposed to be visible globally, don't put it on one of the pages. Instead, stick it in the root window.

Related

Why am I unable to pass a Radiobutton variable from one class to different class using Tkinter?

Let me start this off by saying that I have scoured stack overflow and I have been unable to find an answer to my issue. I have found something that is very close, but I still am unable to resolve my issue. If you are aware of a link that would be helpful, please include it in the comments.
I am relatively new to programming in Python so please provide constructive criticism if you see any incorrect programming practices or if anything can be programmed in a more pythonic style.
I'm having a problem with getting a variable from a radio button in one class and printing it out in a label on a different frame in a different class. FYI: As of right now, all classes are in the same file but I plan to change that after I complete the program.
Here is my controller class:
import tkinter as tk
from tkinter import ttk
class MainWindow(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.emotion_list = ["angry", "sad", "happy", "fearful", "surprised", "curious",
"anxious", "chill"]
self.title("Application")
self.frames = {}
inner_container = ttk.Frame(self)
inner_container.grid(sticky='ew')
for FrameClass in (ChooseFeelings, ChooseState):
frame = FrameClass(inner_container, self, self.pages)
self.frames[FrameClass] = frame
frame.grid(row=0, column=0, sticky='nsew')
self.show_frame(ChooseFeelings)
def show_frame(self, inner_container):
frame = self.frames[inner_container]
frame.tkraise()
def get_frame(self, classname):
"""Returns an instance of a frame given its class name as a string"""
for page in self.frames.values():
if str(page.__class__.__name__) == classname:
return page
return None
As you can see, I have included a method at the bottom of my controller class that can be called to create an instance of any class when needed.
I have also not included all classes in the self.frames dictionary because they don't pertain to the issue at hand.
Here I have the class that displays the radio buttons to the user. I am well aware that as it looks right now, they aren't organized on the frame. There are other widgets on the frame that I am omitting from the class because they don't pertain to the issue.
class ChooseFeelings(ttk.Frame):
def __init__(self, container, controller, **kwargs):
super().__init__(container, **kwargs)
emotion_container = ttk.Frame(self, padding='100 15 30 15')
emotion_container.grid(sticky='ew')
self.controller = controller
self.selected_emotion = tk.StringVar()
move_on_button = ttk.Button(emotion_container, text="Click me when you are ready to move on",
command=lambda: [controller.show_frame(ChooseState), print(selected_emotion.get())])
move_on_button.pack(side='top')
for emotions in self.controller.emotion_list:
ttk.Radiobutton(emotion_container, text=emotions, variable=self.selected_emotion,
value=emotions).pack(side='bottom')
After clicking the move_on_button, I print out to the console the variable that I hope to get in the label on the next frame.
Here is the frame where the variable should print out to the user.
class ChooseState(ttk.Frame):
def __init__(self, container, controller, **kwargs):
super().__init__(container, **kwargs)
self.controller = controller
state_container = ttk.Frame(self, padding='100 15 30 15')
state_container.grid(sticky='ew')
self.emotion_label = ttk.Label(state_container, text=f"You chose {self.get_feels()} as your emotion.")
self.emotion_label.grid(row=0, column=0, sticky='w')
def get_feels(self):
feelings_frame = self.controller.get_frame("ChooseFeelings")
user_emotion = feelings_frame.selected_emotion.get()
return user_emotion
root = MainWindow()
root.mainloop()
As you can see, the get_feels method is used to create an instance of the ChooseFeelings class, then the variable 'user_emotion' is supposed to hold the selected radio button variable. Once that happens, the 'user_emotion' variable is returned and should be printed out to the screen. Or so I thought....
In the ChooseState class, I should be printing out in 'self.emotion_label', the chosen radio button variable from the previous class. Instead all I get is "You chose as your emotion". All I would like to do is pass the radio button variable from the previous class to this class without any errors.
If anyone has any insight on how to fix this problem it would be greatly appreciated. Thank You!

How can I correctly implement tkinter's .after() method in the context of my program?

I am creating a simple GUI program that utilizes Python and Tkinter to log the time/date when a user presses a button on the interface (by appending information to a .txt file), as well as sending an e-mail to a list of addresses informing the recipients that the log has been updated.
The program has three main frames/screens that I would like the user to navigate through. The navigation between the screens should be time-based. In other words, I would like the user to be redirected from the main screen to a secondary screen upon the press of a Tkinter button (which I have already established using the 'command' argument of the Tkinter widgets), and then be automatically redirected back to the main screen after a 5-second time delay.
I understand that using time.sleep() is not encouraged in GUI programs. However, I have had some trouble implementing Tkinter's .after() method, and haven't quite been able to achieve my desired result.
I have attached a simplified example of my program code that models my problem:
import tkinter as tk
class mainApplication(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)
self.frames = {}
for F in (MainScreen, AcknowledgeScreen):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(MainScreen)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class MainScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Press Button to Log Time")
label.pack()
button = tk.Button(self, text="Confirm", command=lambda: controller.show_frame(AcknowledgeScreen))
button.pack()
class AcknowledgeScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Logging Completed. Please Wait.")
label.pack()
# The implementation below is giving me trouble.
self.after(5000, controller.show_frame(MainScreen))
root = mainApplication()
root.mainloop()
The other solutions I have attempted (for the line of interest) include:
# Attempt 1
self.after(5000, controller.show_frame(MainScreen)) # This code waits 5 seconds before opening the GUI window; does not redirect back to MainScreen from AcknowledgeScreen.
# Attempt 2
root.after(5000, controller.show_frame(MainScreen)) # This code throws an error 'NameError: name 'root' is not defined.
# Attempt 3
label.after(5000, controller.show_frame(MainScreen)) # This code waits 5 seconds before opening the GUI window; does not redirect back to MainScreen from AcknowledgeScreen.
Unfortunately, I have never been exposed to object-oriented programming before beginning with Tkinter, so I believe that my errors might be due to a fundamental misunderstanding of how OOP works. Nonetheless, I would appreciate if anyone could point me in the right direction or clarify my errors.
In mainApplication, both screens are initialized and their classes used for dictionary keys mapping to their instances.
From the ordering of your operations, the MainScreen should be raised in the stacking order after AcknowledgeScreen is displayed.
This operation shouldn't live in the AcknowledgeScreen.__init__ except you initialize the screens at the time they are needed.
You want to move this to MainScreen. You can refactor the MainScreen the following way.
class MainScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="Press Button to Log Time")
label.pack()
button = tk.Button(self, text="Confirm", command=self.confirm)
button.pack()
def confirm(self):
self.controller.show_frame(AcknowledgeScreen)
self.after(5000, self.back)
def back(self):
self.controller.show_frame(MainScreen)
after() (like bind() and command=) needs callback - it means function name without () and without arguments.
Use lambda
self.after(5000, lambda:controller.show_frame(MainScreen))
But I see different problem.
When you run program then it creates instances of all frames in mainApplication.__init__ so it runs also AcknowledgeScreen.__init__ at start - and it runs after() at start. It doesn't wait for your click.
BTW: because frames are create inside mainApplication.__init__ so they can't use root which will be created after mainApplication.__init__ ends its job.
You would have to add some parts in methods and run everytime when you click button.
class MainScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="Press Button to Log Time")
label.pack()
button = tk.Button(self, text="Confirm", command=self.change)
button.pack()
def change(self):
self.controller.show_frame(AcknowledgeScreen)
self.controller.frames[AcknowledgeScreen].change_after()
class AcknowledgeScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="Logging Completed. Please Wait.")
label.pack()
def change_after(self):
# The implementation below is giving me trouble.
#both works
#root.after(5000, lambda:self.controller.show_frame(MainScreen))
self.after(5000, lambda:self.controller.show_frame(MainScreen))
Let's look at this code:
self.after(5000, controller.show_frame(MainScreen))
The above code does exactly the same thing as this:
result = controller.show_frame(MainScreen)
self.after(5000, result)
Notice what happens? The function controller.show_frame(MainScreen) executes immediately, rather than being executed by after.
Instead, you need to give after a callable -- roughly speaking, a reference to a function. If that function requires additional arguments, you can add those arguments when calling after:
self.after(5000, controller.show_frame, MainScreen)
The above code tells after to run controller.show_frame in five seconds, and to pass it the argument MainScreen when it is called.

Frame not loading with tkinter

I am trying to code a very simple game using tkinter in python3. It is a sequential game, played for many periods. My problem is that at the end of a period, when clicking the "next period" button, the next period is not loading. Here is my code:
import tkinter as tk
from tkinter import ttk
class Game(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 = {}
F=Period
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky='nsew')
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class Period(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
firstOffer = 5
labelOffer = tk.Label(self, anchor='center', text=str(firstOffer))
labelOffer.place(anchor='center', relx=0.6, rely=0.5)
buttonAcc = ttk.Button(self, text='Accept', command=lambda: self.PeriodSummary(parent, controller))
buttonAcc.place(anchor='center', relx=0.35, rely=0.5)
def PeriodSummary(self, parent, controller):
buttonNextPeriod = ttk.Button(self, text='Next Period', command=lambda: controller.show_frame(Period))
buttonNextPeriod.place(anchor='center', relx=0.5, rely=0.75, width=200, height=80)
app = Game()
app.wm_state('zoomed')
app.mainloop()
As can be seen when the code is run, when the user clicks on "next period" nothing happens. I would like it to start a new period. I believe that I have an incorrect understanding of object orieted programming in Python. Any pointers would be highly appreciated, thank you.
Contrary to your comment above, putting print(self.frames) inside show_frame shows that show_frame is being called. Since self.frames has only one item, {<class '__main__.Period'>: <__main__.Period object .!frame.!period>}, and the single Period object is already visible, .tkraise exposes what is already exposed and there is no visible change. There are two ways to add dynamism.
The easy option: make one period frame, expose it, and have the Next button change the content. Do this if you possibly can.
The harder option: make multiple period frames, either all at once or as needed. Each would have a visibly different content. Expose the first. Then have the button expose the next. There are two suboptions: stack identically sized gridded (or packed or placed) frames on top of eacy other and raise the one you want to see; or only grid the one you want to see, ungridded when making the switch. I would only go the multiple-frame route if you need to re-expose frames seen before. An if you need this, use a tabbed frame widget, such as ttk.Notebook, that handles the stacking and exposing for you. An example is a windows with multiple forms to fill, each of which may get multiple uses. A particular example is IDLE's tabbed configuration dialog.

Entry widget contents not changing with function call

I need to change the content of an entry whenever the tkinter frame is shown. Below is what I have so far, and it doesn't seem to work. I have tried to use data = self.read() and then now.insert(0, data) and that has not worked either. If the value is displayed then it doesn't get changed every time the class ReadLabel1 is called.
class ReadLabel1(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent, bg="blue")
label = tk.Label(self, text="SomeData:", font = "Times 12", bg="blue")
label.place(x=10, y=100) #ack(pady=5,padx=30)
self.smStr = tk.StringVar()
now=tk.Entry(self, width=22, textvariable=self.read())
now.place(x=120, y=103)
def read(self):
# code to get data
return data
You need to turn 'change the content of an entry' into a one parameter callback, turn 'whenever the tkinter frame is shown' into an event, and then bind together the app, the event, and the callback. Here is a minimal example.
import time
import tkinter as tk
root = tk.Tk()
now = tk.StringVar()
lab = tk.Label(root, textvariable=now)
lab.pack()
def display_now(event):
now.set(time.ctime())
root.bind('<Visibility>', display_now)
root.bind('<FocusIn>', display_now)
Minimizing the window to a icon and bringing it back up triggers the Visibility event. Covering and merely uncovering with a different window did not, at least not with Windows. Clicking on the uncovered, or merely inactivated, window triggered FocusIn. You can experiment more with your system. I used this tkinter reference

How do you validate a window in Tkinter?

def createWidgets(self):
self.INSTRUCTIONS = Button(self) #creating button linked to instructions_window
self.INSTRUCTIONS["text"] = "Instructions"
self.INSTRUCTIONS["fg"] = "green"
self.INSTRUCTIONS["command"] = self.instruction_window #command which opens instructions_window
self.INSTRUCTIONS.pack({"side": "left"})
Currently, if I press the button multiple times then the instructions window will open multiple times. How do I ensure that when the button is pressed, if the window is already open then it will flash to show that the same window can't be opened. Is there a command? Or do I need to use a validation of some sort?
Here's a great article dealing with more complicated examples of dialog boxes.
Essentially what you are looking for is almost like a modal dialog window except it seems with the additional ability to still interact with the parent window to a degree. At this point it may be worth considering making it totally modal, but I do not know your use case. If not, you can definitely adapt the scripts given on the tutorial website to fit your needs.
The way I do this is to create a function that will create the window if it doesn't exist, and then display the window. Personally I don't think there's a need to flash the window, but you could do that if you want. Tkinter doesn't know how to flash a window, but you can do something simple like changing the colors briefly.
Here's an example:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.instruction_window = None
self.instructions = tk.Button(self, text="Instructions", foreground="green",
command=self.show_instructions)
self.instructions.pack(side="left")
def show_instructions(self):
'''show the instruction window; create it if it doesn't exist'''
if self.instruction_window is None or not self.instruction_window.winfo_exists():
self.instruction_window = InstructionWindow(self)
else:
self.instruction_window.flash()
class InstructionWindow(tk.Toplevel):
'''A simple instruction window'''
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.text = tk.Text(self, width=40, height=8)
self.text.pack(side="top", fill="both", expand=True)
self.text.insert("end", "these are the instructions")
def flash(self):
'''make the window visible, and make it flash temporarily'''
# make sure the window is visible, in case it got hidden
self.lift()
self.deiconify()
# blink the colors
self.after(100, lambda: self.text.configure(bg="black", fg="white"))
self.after(500, lambda: self.text.configure(bg="white", fg="black"))
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()

Categories

Resources