How to switch between frames in tkinter - python-3.x

Currently resisting the temptation to throw my laptop out of the window and smash it with a bat at this point.
Currently, I'm trying to create a simple GUI for what used to be a nice simple text based RPG game. But trying to work with a GUI makes me want to die.
I just want to have a scaleable way to swap between frames in the game. (Currently there exists the main menu and the Work in progress character creation screen because I can't even manage to get even just that to work.)
I've tried most things that I can find on this website and on discord servers and I seem to just get a new error every time.
I just want to know how to swap between these since trying anything that I can find online just creates more errors.
There are more "screens" to come since it's a game so a scaleable solution would be perfect thanks.
import tkinter
from tkinter import *
from tkinter import ttk
from PIL import ImageTk, Image
root = Tk()
content = ttk.Frame(root)
root.geometry("600x600")
class CharacterCreate(tkinter.Frame):
def __init__(self, parent):
tkinter.Frame.__init__(self)
self.parent = parent
backgroundchar = ImageTk.PhotoImage(Image.open("plont2.png"))
backgroundlabelchar = tkinter.Label(content, image = backgroundchar)
backgroundlabelchar.image = backgroundchar
backgroundlabelchar.grid(row=1,column=1)
Charname = tkinter.Label(content, text = "Enter your character name here:").grid(row=0)
e1 = tkinter.Entry(content)
e1.grid(row=0, column=1)
e1.lift()
CharBtn1 = Button(content, text="Return to main menu", width = 15, height = 1)
CharBtn1.grid(row=2, column=2)
CharBtn1.lift()
class MainMenu(tkinter.Frame):
def __init__(self, parent):
tkinter.Frame.__init__(self)
self.parent = parent
background = ImageTk.PhotoImage(Image.open("bred.png"))
content.grid(column=1, row=1)
Btn1 = Button(content, text="Play", width=5, height=1, command = CharacterCreate.lift(1))
Btn2 = Button(content, text="Quit", width=5, height=1, command = root.quit)
backgroundlabel = tkinter.Label(content, image=background)
backgroundlabel.image = background
backgroundlabel.grid(row=1, column=1)
Btn1.grid(row=1, column=1, padx=(50), pady=(50))
Btn1.columnconfigure(1, weight=1)
Btn1.rowconfigure(1, weight=1)
Btn1.lift()
Btn2.grid(row=1, column=2, padx=(50), pady=(50))
Btn2.columnconfigure(2, weight=1)
Btn2.rowconfigure(1, weight=1)
Btn2.lift()
MainMenu(1)
root.mainloop()

You have five major problems:
you are calling a function immediately (command=CharacterCreate.lift(1)) rather than at the time the button is clicked (command=CharacterCreate.lift),
you are passing an invalid argument to lift - you are passing 1, but the argument to lift must be another widget,
you are calling lift on a class rather than an instance of a class.
you never create an instance of CharacterCreate
your classes inherit from Frame but you never use the classes as frames -- they each place their widgets directly in container
Switching between pages usually involves one of two techniques: create all the frames at startup and then lift the active frame above the others, or destroy the current frame and recreate the active frame. You seem to be attempting to do the latter, so this answer will show you how to do that.
Since fixing your program is going to require many changes, I am instead going to show you a template that you can use to start over.
Let's start with an import, and then the definition of your pages. To keep the example short, each class will have a single label so that you can distinguish between them (note: importing tkinter "as tk" is done simply to make the code a bit easier to read and type):
import tkinter as tk
class CharacterCreate(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="I am CharacterCreate")
label.pack(padx=20, pady=20)
class MainMenu(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="I am MainMenu")
label.pack(padx=20, pady=20)
Your original code created a container, so we'll do that next. We need to create the root window, too:
root = tk.Tk()
container = tk.Frame(root)
container.pack(fill="both", expand=True)
Now we need to create an instance of each page, giving them the container as the parent. As a rule of thumb, the code that creates a widget should be the code that calls pack, place, or grid on the widget, so we have to do that too. We need to make sure that grid is configured to give all weight to row 0 column 0.
main = MainMenu(container)
cc = CharacterCreate(container)
main.grid(row=0, column=0, sticky="nsew")
cc.grid(row=0, column=0, sticky="nsew")
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
We need a way to lift one of the classes above the other. That's best handled by a function. To make the code easier to understand we'll save the pages in a dictionary so we can reference them by name. This name will be the argument to the function.
pages = {"cc": cc, "main": main}
def switch(name):
page = pages[name]
page.lift()
Finally, we need to start with the main menu on top, and we need to start the event loop:
switch('main')
root.mainloop()
With that, you have a program that runs and displays the main menu. To finish the example lets add a button to the menu to switch to the create page, and create a button in the create page to switch back to the menu.
First, inside the __init__ of MainMenu add the following after the code that creates the label. Notice that because we need to pass an argument to switch, we use lambda:
button = tk.Button(self, text="Go to creater", command=lambda: switch('cc'))
button.pack()
And next, inside the __init__ of CharacterCreate add the following after the code that creates the label:
button = tk.Button(self, text="Go to main menu", command=lambda: switch('main'))
button.pack()
With that, you now have the basic structure to create as many pages as you want, and easily switch to them by name.

Related

tkinter text widget increasing size of frame

I am trying to create a GUI for an application I am making and for some reason that I cannot figure out, the text widget that is inside the message_space frame is increasing the size of the message_space frame and reducing the size of the friends_space frame. I want the friends_space frame to take up 1/4th of the window size and the message_space frame to take up the remaining 3/4ts of the window size.
The red is the friends_space frame, the blue is the message_space frame.
This is how I would like the sizing of the frames to be.
This is what is happening when I add the text box.
Code
from tkinter import *
class app:
def __init__(self, master):
self.master = master
master.title("PyChat")
master.geometry("800x500")
master.configure(bg="grey")
master.resizable(0, 0)
master.grid_columnconfigure(0, weight=1)
master.grid_columnconfigure(1, weight=3)
master.grid_rowconfigure(0, weight=1)
self.friends_space = Frame(master, bg="red")
self.friends_space.grid(row=0, column=0, sticky=NSEW)
self.chat_space = Frame(master, bg="blue")
self.chat_space.grid(row=0, column=1, columnspan=3, sticky=NSEW)
self.message_area = Text(self.chat_space)
self.message_area.grid(row=0, column=0)
root = Tk()
my_gui = app(root)
root.mainloop()
If you're using grid, you divide your UI into four uniform-width columns (using the uniform option), then have the text widget span three.
You should also start with a small text widget that can grow into the space. Otherwise tkinter will try to preserve the large size and start removing space from the other widgets in order to try to make everything fit.
Here's an example based on your original code. However, I'm using pack for the text widget instead of grid because it requires fewer lines of code. I've also reorganized the code a bit. I find that grouping calls to grid together makes layout easier to grok.
I've also removed the restriction on resizing. There's rarely a good idea to limit the user's ability to resize the window. Plus, it allows you to see that the resulting UI is responsive.
from tkinter import *
class app:
def __init__(self, master):
self.master = master
master.title("PyChat")
master.geometry("800x500")
master.configure(bg="grey")
master.grid_columnconfigure((0,1,2,3), uniform="uniform", weight=1)
master.grid_rowconfigure(0, weight=1)
self.friends_space = Frame(master, bg="red")
self.chat_space = Frame(master, bg="blue")
self.friends_space.grid(row=0, column=0, sticky=NSEW)
self.chat_space.grid(row=0, column=1, columnspan=3, sticky=NSEW)
self.message_area = Text(self.chat_space, width=1, height=1)
self.message_area.pack(fill="both", expand=True)
root = Tk()
my_gui = app(root)
root.mainloop()

Message widget not filling frame using tkinter

I'm creating a simple user dialog window with a basic text on top and a tree view with one column below, that gives the user a couple of choices. A button at the bottom is used to confirm the selection.
Now I can't get the Message widget, which I use to display the instructions, to fill the Frame I've created for it. Meanwhile, the Treeview widget fills the Frame as I want it to.
Many proposed solutions on other StackOverflow questions state, that putting my_message.pack(fill=tk.X, expand=True) should work. It doesn't in my case.. In a different scenario it is recommended to put my_frame.columnconfigure(0, weight=1), which doesn't help either.
Here is the code:
import tkinter as tk
from tkinter import ttk
class MessageBox(object):
""" Adjusted code from StackOverflow #10057662. """
def __init__(self, msg, option_list):
root = self.root = tk.Tk()
root.geometry("400x400")
root.title('Message')
self.msg = str(msg)
frm_1 = tk.Frame(root)
frm_1.pack(expand=True, fill=tk.X, ipadx=2, ipady=2)
message = tk.Message(frm_1, text=self.msg)
message.pack(expand=True, fill=tk.X) # <------------------------------------ This doesn't show the desired effect!
frm_1.columnconfigure(0, weight=1)
self.tree_view = ttk.Treeview(frm_1)
self.tree_view.heading("#0", text="Filename", anchor=tk.CENTER)
for idx, option in enumerate(option_list):
self.tree_view.insert("", idx+1, text=option)
self.tree_view.pack(fill=tk.X, padx=2, pady=2)
choice_msg = "Long Test string to show, that my frame is unfortunately not correctly filled from side to side, as I would want it to."
choices = ["Test 1", "Test 2", "Test 3"]
test = MessageBox(choice_msg, choices)
test.root.mainloop()
I'm slowly going nuts, because I know that there is probably something very basic overruling the correct positioning of the widget, but I've been trying different StackOverflow solutions and browsing documentation for hours now with no luck.
Try to set a width of message in the tk.Message constructor, something like this:
message = tk.Message(frm_1, text=self.msg, width=400-10) # 400 - is your window width
message.pack() # In that case you can delete <expand=True, fill=tk.X>
The problem you are facing is a feature: The Message widget tries to lay out the text in one of two ways:
according to an aspect (width-to-height ratio in percent)
according to a maximum width (lines will be broken if longer)
Both of these goals seem not to cooperate well with the automatic resizing a Message widget experiences from a grid or pack layout manager. What can be done is to bind a handler to the resize event of the widget to adjusts the width option dynamically. Also, there are better options to use with the pack layout manager than shown in OP.
I derived an AutoMessage widget to get the event handler out of the way:
import tkinter as tk
from tkinter import ttk
class AutoMessage(tk.Message):
"""Message that adapts its width option to its actual window width"""
def __init__(self, parent, *args, **options):
tk.Message.__init__(self, parent, *args, **options)
# The value 4 was found by experiment, it prevents text to be
# displayed outside of the widget (exceeding the right border)
self.padx = 4 + 2 * options.get("padx", 0)
self.bind("<Configure>", self.resize_handler)
def resize_handler(self, event):
self.configure(width=event.width - self.padx)
class MessageBox(object):
"""Adjusted code from StackOverflow #10057662."""
def __init__(self, msg, option_list):
root = self.root = tk.Tk()
root.geometry("400x400")
root.title("Message")
self.msg = str(msg)
self.frm_1 = tk.Frame(root)
self.frm_1.pack(side=tk.TOP, fill=tk.X, padx=2, pady=2)
self.message = AutoMessage(self.frm_1, text=self.msg, anchor=tk.W)
self.message.pack(side=tk.TOP, fill=tk.X)
self.frm_1.columnconfigure(0, weight=1)
self.tree_view = ttk.Treeview(self.frm_1)
self.tree_view.heading("#0", text="Filename", anchor=tk.CENTER)
for idx, option in enumerate(option_list):
self.tree_view.insert("", idx + 1, text=option)
self.tree_view.pack(fill=tk.X, padx=2, pady=2)
choice_msg = "Long Test string to show, that my frame is unfortunately not correctly filled from side to side, as I would want it to."
choices = ["Test 1", "Test 2", "Test 3"]
test = MessageBox(choice_msg, choices)
test.root.mainloop()

tkinter GUI with or without object inheritance

I am writing a Python program with a GUI and have selected tkinter to do so. I have some experience with Python, but I am new to tkinter and GUI programming.
I have been reading a few tutorials about how to use tkinter. What I could find was very basic, such as "how to display a button". This leaves me struggling to identify a useful model for structuring the part of my program that defines the UI.
So far my searches only yielded 1 guide for structuring a python/tkinter GUI in an OOP style: pythonprogramming.net
Although this is a welcome example and very helpful in its specificity, it seems to me as though the inheritance of tkinter classes and adding new unrelated code to those new classes violates a strict separation of concerns. It looks very convenient on the short term, but I can't tell if it has undesirable consequences long-term.
As an alternative I created another example, in which I made similar classes, but avoided inheriting from tkinter classes, by composing various tkinter objects. This keeps functionality separated with only a couple of additional methods.
I would appreciate feedback on which approach is more useful as a UI grows in complexity. This could include specific suggestions on other models to use, links to information on the subject, source code examples of programs using tkinter, and so on.
Example of inheritance based on pythonprogramming.net:
import tkinter as tk
class AppMain(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 = {}
frame = Page(container, self)
self.frames[Page] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(Page)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class Page(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Start Page", font=("Verdana", 12))
label.pack(padx=10, pady=10)
def main():
application = AppMain()
application.mainloop()
if __name__ == "__main__":
main()
Alternative without inheritance:
EDIT 1: Add grid variable to Page init
import tkinter as tk
class AppMain(object):
def __init__(self):
self.root = tk.Tk()
container = tk.Frame(self.root)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.pages = {}
page = Page(container, self.root, {"row": 0, "column": 0, "sticky": "nsew"})
self.pages[Page] = page
self.show_page(Page)
def show_page(self, container):
page = self.pages[container]
page.show()
def run(self):
self.root.mainloop()
class Page(object):
def __init__(self, parent, controller, grid):
self.frame = tk.Frame(parent)
self.frame.grid(**grid)
label = tk.Label(self.frame, text="Start Page", font=("Verdana", 12))
label.pack(padx=10, pady=10)
def show(self):
self.frame.tkraise()
def main():
application = AppMain()
application.run()
if __name__ == "__main__":
main()
The main advantage of inheriting from a tkinter widget (typically a Frame) is so that you can treat that object like any other widget when it comes to laying out your UI.
For example, a typical UI might be made of a toolbar, a side panel for navigation, a main work area, and perhaps a statusbar at the bottom. By creating a class for each of these which inherits from Frame, you can lay out your GUI like so:
toolbar = Toolbar(root)
sidebar = Sidebar(root)
main = WorkArea(root)
statusbar = Statusbar(root)
toolbar.pack(side="top", fill="x")
statusbar.pack(side="bottom", fill="x")
sidebar.pack(side="left", fill="y")
main.pack(side="right", fill="both", expand=True)
If you use composition instead of inheritance, then either your main program needs to know something about the internal structure of your objects, or your objects need to know something about the root window.
For example, you might have to name your inner frame of each section with a common name so that the main program can lay it out:
toolbar.inner_frame.pack(side="top", fill="x")
statusbar.inner_frame.pack(side="bottom", fill="x")
sidebar.inner_frame.pack(side="left", fill="y")
main.inner_frame.pack(side="right", fill="both", expand=True)
In the example in your question where you inherit from object, your Page class has to know that the root window is using grid, and further has to know that it need to place itself in a specific row and column. This tightly couples these two parts of your code together -- you can't modify one part of your code without having to modify other parts.
For example, let's say you have a dozen pages. After working on the code for a while you decide that AppMain needs to place an additional widget in row zero of the container. You now have to go in and modify all dozen page classes so that they can place themselves in row 1 instead of row 0.

Can not move widgets in Tkinter

I am trying to learn tkinter, but I got a problem and I can`t move forward from this point. I wanted to make just a simple GUI with one button, unfortunately, I am not able to move that button ( being always displayed in the most left upper corner).
This is the code that I used :
class App(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.grid()
self.master.title('GUI')
quitbttn = Button(self, text='quit')
quitbttn.grid(row=3, column=5)
root = Tk()
app = App(root)
app.mainloop()
Although, I found this snippet of code on the iternet, and it is working perfectly, the only difference being that pack() is used instead of grid() :
class Window(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.master = master
self.init_window()
def init_window(self):
self.master.title("GUI")
self.pack(fill=BOTH, expand=1)
quit_button = Button(self, text='quit')
quit_button.pack(side=BOTTOM)
root = Tk()
app = Window(root)
root.mainloop()
I would like to be able to use grid as well.
Any advice is being apreciated. Thank you!
The reason you cannot move the button is because you only have one element.
quitbttn.grid(row=3, column=5)
This part of the script basically says that the button should be placed a third row down and in the fifth space along. Since you have no other elements in the window it does not move the button at all. This is because all the 2 rows and 4 columns are all equal to 0 so the first place it packs is in the top left corner.
Using the .pack() function allows you move the button without the need of any other button in the window.
If you added another button you would then be able to move around the first button in three different places.
Note that you cannot use the .pack() and .grid() functions in the same window.

Tkinter - Understanding how to switch frames

Im trying to teach my self how to use tkinter and I found a useful code through youtube that I don't really fully understand. Would appreciate it if some could help me understand it. Marked the things I did not understand with # ... **.
import tkinter as tk # why not from tkinter import? **
class SampleApp(tk.Tk): # why tk.TK **
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
# the container is where we'll stack a bunch of frames
# on top of each other, then the one we want visible
# will be raised above the others
container = tk.Frame(self) # **
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (StartPage, PageOne, PageTwo):
page_name = F.__name__
frame = F(container, 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() # **
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=TITLE_FONT)
label.pack(side="top", fill="x", pady=10)
button1 = tk.Button(self, text="Go to Page One",
command=lambda: controller.show_frame("PageOne"))
button2 = tk.Button(self, text="Go to Page Two",
command=lambda: controller.show_frame("PageTwo"))
button1.pack()
button2.pack()
import tkinter as tk # why not from tkinter import? **
Why not from tkinter import *? Because that is the wrong way to do it. Global imports are bad. Tkinter tutorials tend to do it the wrong way for some reason that I don't understand.
The reason for the as tk part is so that you can do tk.Frame rather than tkinter.Frame, making the code a little easier to type and a little easier to read. It's completely optional.
class SampleApp(tk.Tk):
tk is the name of the tkinter module that was imported. Tk is the name of a class in that module that represents the root window. Every tkinter application must have a single root window. By placing it inside of SampleApp, this creates a subclass of this widget -- a copy that has additional features.
It's not necessary to inherit from tk.Tk. You can inherit from tk.Frame, any other tkinter widget, or even object. It's a personal preference. The choice makes some things easier, some things harder.
container = tk.Frame(self)
The above creates an instance of a Frame widget, which will be used as a container for other "pages". These "pages" will all be stacked on top of each other in this container.
frame = F(container, self)
F is the loop variable. The loop is iterating over a list of classes, so each time through the loop F will be representing a class. F(...) creates an instance of the class. These classes (StartPage, PageOne, PageTwo) all require two parameters: a widget that will be the parent of this class, and an object that will server as a controller (a term borrowed from the UI patter model/view/controller).
The line of code creates an instance of the class (which itself is a subclass of a Frame widget), and temporarily assigns the frame to the local variable frame.
By passing self as the second ("controller") parameter, these new class instances will be able to call methods in the SampleApp class object.
This saves a reference to the just-created frame in a dictionary. The key to the dictionary is the page name (or more accurately, the name of the class).
This is how the show_frame method can determine the actual page widget just from the name of the class.
Creating the frames in a loop is functionally equivalent to the following:
f1 = StartPage(container, self)
f2 = PageOne(container, self)
f3 = PageTwo(container, self)
f1.grid(row=0, column=0, sticky="nsew")
f2.grid(row=0, column=0, sticky="nsew")
f3.grid(row=0, column=0, sticky="nsew")
self.frames = {"StartPage": f1, "PageOne": f2, "PageTwo": f3}
frame.tkraise()
In nearly all GUI toolkits -- tkinter included -- there is the notion of a "stacking order": the order in which things are stacked. Some toolkits might call this the z-order. If two or more widgets are stacked on top of each other (which this code does by putting all pages in the same row and column), the widget that is on the top of the stack is the widget that will typically be visible.
tkraise is a method of a Frame object that will raise the frame to the top of the stacking order. In this line of code, frame refers to one particular instance of one of the pages.
tk.Frame.__init__(self, parent)
Because each page is a subclass of a tk.Frame class, this calls the constructor of the parent class. This is necessary to initialize all of the internal structures that make up the actual frame widget. Although a Frame can take many options, this code chooses to send in only one -- a reference to another widget which is to act as the parent of this new widget.
self.controller = controller
The above code is simply "remembering" the value of the controller variable which was passed in. In this case, the controller is the application. By saving it off, this class can call methods on the SampleApp object.
Note: the code in the question came from a tutorial that copied code from this answer: https://stackoverflow.com/a/7557028/7432. I am the author of that original code, but not the author of the tutorial. In that original answer are links to other questions related to this code.

Resources