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.
Related
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.
I've tried for 2 days to get this to work with no luck. I've tried all the methods found on related questions - destroy, quit, sys.exit... nothing's working. The issue is that my code seems to be written in a slightly different way than tkinter code is usually written on SO, because I'm using a separate class for each window, on top of the main class. I'm therefore having difficulty seeing where the "root" variable used in similar questions corresponds to my code (the common suggestion is "command=root.quit" or similar). However, I'm sure the structure of the code is fine (the app runs fine) as I copied the basic framework from sentdex's "Multiple Windows/Frames in Tkinter GUI with Python" video.
The code is as follows, with irrelevant parts of "StartPage" removed. There is also "PageOne" and "PageTwo", not included, which are very similar to StartPage. I will need the same button on these two pages.
The for loop and show_frame method in the main class is used for switching between the frames via buttons. This all works fine.
class testApp(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 (StartPage, PageOne, PageTwo):
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)
...
confirmFrame = tk.Frame(self)
confirmFrame.pack()
...
self.buttonNo = tk.Button(confirmFrame, text="No, quit the program",
command=????)
self.buttonNo.pack()
app = testApp()
app.mainloop()
For full clarity then, the button "buttonNo" in StartPage needs to quit the entire program, via some command which I cannot figure out.
Thank you for any help you can offer.
I know of 2 ways:
To obtain the toplevel (your window) use self.winfo_toplevel(), so self.winfo_toplevel().quit() should end your application.
Less nice is using self._root().quit() which uses a private function (see the leading underscore on _root), so may be changed without warning.
Your testApp inherits from tk.Tk, so it becomes the root. And it gets passed as the controller arg, so your frames can use that name to access it. Here's a quick demo derived from your code.
import tkinter as tk
class testApp(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 (StartPage,):
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)
confirmFrame = tk.Frame(self)
confirmFrame.pack()
self.buttonNo = tk.Button(confirmFrame, text="No, quit the program",
command=controller.quit)
self.buttonNo.pack()
app = testApp()
app.mainloop()
BTW, it makes it a lot easier for us to help you if you post a MCVE. Otherwise we need to spend time figuring out what we need to add & remove from your code before we can even run it.
I am trying to open a toplevel widget from a button press, and generate a list within that frame from an example I found. However, when I try to place the widget within the generated frame, I get the following error:
_tkinter.TclError: can't put .!treeview inside .!errorexample.!toplevel.!mclistdemo.!frame.!frame
I have narrowed down the problem to
self.tree.grid(in_=f, row=0, column=0, sticky=NSEW)
within the _create_treeview method. When the in_ command is removed, the widget is generated correctly in the parent window. I suspect that the problem has something to do with my parent/self naming conventions, but I am still struggling to grasp that subject.
Most of the questions I've run across that are described similarly seem to be a matter of trying to place the widget while generating it, but that doesn't seem to be the case in this code.
from tkinter import *
from tkinter import ttk
from tkinter.font import Font
class ErrorExample(Frame):
def __init__(self, parent):
Frame.__init__(self,parent)
self.grid()
self.parent=parent
self.b4=Button(
self,
text="Load",
command=lambda: self.createWindow())
self.b4.grid()
def createWindow(self):
self.t = Toplevel(self)
MCListDemo(self)
class MCListDemo(ttk.Frame):
def __init__(self, parent, isapp=True):
ttk.Frame.__init__(self, parent.t)
self.grid()
self.isapp = isapp
self._create_widgets()
def _create_widgets(self):
if self.isapp:
self._create_demo_panel()
def _create_demo_panel(self):
demoPanel = Frame(self)
demoPanel.grid()
self._create_treeview(demoPanel)
self._load_data()
def _create_treeview(self, parent):
f = ttk.Frame(parent)
f.grid()
self.dataCols = ('country', 'capital', 'currency')
self.tree = ttk.Treeview(columns=self.dataCols,
show = 'headings')
self.tree.grid(in_=f, row=0, column=0, sticky=NSEW)
start=Tk()
ErrorExample(start)
if __name__=="__main__":
main()
You don't give the treeview a parent, so it has the root window as a parent. Widgets live in a hierarchy, and widget's can't be placed in a different part of the hierarchy.
The official documentation describes it like this:
The master for each slave must either be the slave's parent (the default) or a descendant of the slave's parent. This restriction is necessary to guarantee that the slave can be placed over any part of its master that is visible without danger of the slave being clipped by its parent.
If you want the treeview to be in f, the simplest way is to make f be the parent:
self.tree = ttk.Treeview(f, ...)
self.tree.grid(row=0, column=0, sticky=NSEW)
I have try to write program for library Management system I am using tkinter module for it. I have writen the below code but when I am trying to create multiple Text box i am getting below error.
File "Hope_work.py", line 22, in __init__
frame = F(container, self)
File "Hope_work.py", line 62, in __init__
pwd_lable.pack()
UnboundLocalError: local variable 'pwd_lable' referenced before assignment
Below is the complete program I am getting error in PageOne class
import tkinter as tk
import os
LARGE_FONT= ("Verdana", 12)
class Myprogramapp(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, PageOne):
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)
label = tk.Label(self, text="Library Managment System", font=LARGE_FONT)
label.pack(pady=10,padx=10)
button = tk.Button(self, text="Admin Login",
command=lambda: controller.show_frame(PageOne))
button.pack()
button1 = tk.Button(self, text="Lib Login")
button1.pack()
class PageOne(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
name_label = tk.Label(self, text="User ID : ")
pwd_label = tk.Label(self.name_lable, text="Password:")
name_label.pack(pady=10,padx=10)
pwd_lable.pack(pady=10,padx=10)
name_lable = tk.Entry(self)
pwd_lable = tk.Entry(self, show="*")
name_lable.pack()
pwd_lable.pack()
button1 = tk.Button(self, text="Login")
button1.pack()
if __name__ == "__main__":
app = Myprogramapp()
app.mainloop()
**
It would appear that you are trying to bite off more than you can chew so to speak with this example of code. You are miss using parts of tkinter that should be understood before moving onto something more complicated like this.
Before you try to use multiple classes like this try to focus more on understanding how tkinter works and how to implement all its widgets properly in a single class or even outside of a class first.
You do not assign widgets to another widget like you are attempting to do here:
pwd_label = tk.Label(self.name_lable, text="Password:")
This is the problem refereed to in your trackback. You need to assign the Label widget to either a root window, a frame or a toplevel.
Your indention is not clean and if the way you have it pasted in your question is accurate then you code will not work as the def show_frame() method is not inside the Myprogramapp class.
You are importing os for no reason here and it is not good practice to import libraries you are not currently using.
You should make some important parts of your program into a class attribute, like your entry fields. If you are going to put in a password to that field and try and get() the string from the entry field inside a method you wont be able to as its not a class attribute. You can fix this by adding self. prefix to the widget name.
All and all after making those changes you will get a tkinter window with 2 buttons. The Admin Login button will display a login screen. That being said I think you should either step away from classes all together while learning tkinter or work in a single class for now until you have a solid understanding of how classes, methods, and attributes work and are used.
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.