128

I have built my first few scripts with a nice little GUI on them, as the tutorials have shown me, but none of them address what to do for a more complex program.

If you have something with a 'start menu', for your opening screen, and upon user selection you move to a different section of the program and redraw the screen appropriately, what is the elegant way of doing this?

Does one just .destroy() the 'start menu' frame and then create a new one filled with the widgets for another part? And reverse this process when they press the back button?

martineau
  • 119,623
  • 25
  • 170
  • 301
Max Tilley
  • 1,509
  • 3
  • 10
  • 9

4 Answers4

236

One way is to stack the frames on top of each other, then you can simply raise one above the other in the stacking order. The one on top will be the one that is visible. This works best if all the frames are the same size, but with a little work you can get it to work with any sized frames.

Note: for this to work, all of the widgets for a page must have that page (ie: self) or a descendant as a parent (or master, depending on the terminology you prefer).

Here's a bit of a contrived example to show you the general concept:

try:
    import tkinter as tk                # python 3
    from tkinter import font as tkfont  # python 3
except ImportError:
    import Tkinter as tk     # python 2
    import tkFont as tkfont  # python 2

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")

        # 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()


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=controller.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()


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=controller.title_font)
        label.pack(side="top", fill="x", pady=10)
        button = tk.Button(self, text="Go to the start page",
                           command=lambda: controller.show_frame("StartPage"))
        button.pack()


class PageTwo(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is page 2", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)
        button = tk.Button(self, text="Go to the start page",
                           command=lambda: controller.show_frame("StartPage"))
        button.pack()


if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()

start page page 1 page 2

If you find the concept of creating instance in a class confusing, or if different pages need different arguments during construction, you can explicitly call each class separately. The loop serves mainly to illustrate the point that each class is identical.

For example, to create the classes individually you can remove the loop (for F in (StartPage, ...) with this:

self.frames["StartPage"] = StartPage(parent=container, controller=self)
self.frames["PageOne"] = PageOne(parent=container, controller=self)
self.frames["PageTwo"] = PageTwo(parent=container, controller=self)

self.frames["StartPage"].grid(row=0, column=0, sticky="nsew")
self.frames["PageOne"].grid(row=0, column=0, sticky="nsew")
self.frames["PageTwo"].grid(row=0, column=0, sticky="nsew")

Over time people have asked other questions using this code (or an online tutorial that copied this code) as a starting point. You might want to read the answers to these questions:

milanbalazs
  • 4,811
  • 4
  • 23
  • 45
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • 4
    Wow, thank you so much. Just as an academic question rather than a practical one, how many of these pages hidden on top of each other would you need before it started to start becoming laggy and unresponsive? – Max Tilley Sep 27 '11 at 09:18
  • 7
    I don't know. Probably thousands. It would be easy enough to test. – Bryan Oakley Sep 27 '11 at 12:50
  • 1
    Is there no truly simple way of achieving an affect similar to this, with much fewer lines of code, without having to stack frames on top of each other? – Parallax Sugar Jun 14 '17 at 15:11
  • 3
    @LauraCookson: there are definitely simpler ways of achieving this, with or without stacking frames. In the absolute simplest case, create a frame with whatever you want. When you are ready to switch, destroy it and create something else. – Bryan Oakley Jun 14 '17 at 15:15
  • 1
    Aran-Fey pointed out that there is a [`.pack_forget`](https://www.daniweb.com/programming/software-development/code/276574/demonstrating-tkinter-pack-and-pack-forget) method you can use to hide unneeded frames without deleting them. – Stevoisiak Mar 21 '18 at 17:20
  • 3
    @StevenVascellaro: yes, `pack_forget` can be used if you're using `pack`. – Bryan Oakley Mar 21 '18 at 17:45
  • 4
    **Warning**: It is possible for users select "hidden" widgets on background frames by pressing Tab, then activate them with Enter. – Stevoisiak May 21 '19 at 18:35
  • What is the variable stored in controller ? – Jdeep Jun 11 '20 at 13:33
  • @NoahJ.Standerson: I don't know what you mean by "the variable". Which variable are you asking about? – Bryan Oakley Jun 11 '20 at 13:52
  • @BryanOakley The constructor of class `Start Page` requires a third parameter `Controller` . What does this parameter store? – Jdeep Jun 11 '20 at 14:58
  • @NoahJ.Standerson: it stores a reference to the controller. The controller is an object which provides access control between the pages. The controller in this case is the instance of `SampleApp`. – Bryan Oakley Jun 11 '20 at 15:02
  • @yet-it-compiles: it can be easily done, but if that's what you need then this isn't the best way to do it. In such a case you probably should delete the old frame and then instantiate the new frame in `show_frame`. – Bryan Oakley Feb 09 '22 at 16:28
43

Here is another simple answer, but without using classes.

from tkinter import *


def raise_frame(frame):
    frame.tkraise()

root = Tk()

f1 = Frame(root)
f2 = Frame(root)
f3 = Frame(root)
f4 = Frame(root)

for frame in (f1, f2, f3, f4):
    frame.grid(row=0, column=0, sticky='news')

Button(f1, text='Go to frame 2', command=lambda:raise_frame(f2)).pack()
Label(f1, text='FRAME 1').pack()

Label(f2, text='FRAME 2').pack()
Button(f2, text='Go to frame 3', command=lambda:raise_frame(f3)).pack()

Label(f3, text='FRAME 3').pack(side='left')
Button(f3, text='Go to frame 4', command=lambda:raise_frame(f4)).pack(side='left')

Label(f4, text='FRAME 4').pack()
Button(f4, text='Goto to frame 1', command=lambda:raise_frame(f1)).pack()

raise_frame(f1)
root.mainloop()
nbro
  • 15,395
  • 32
  • 113
  • 196
recobayu
  • 581
  • 4
  • 6
39

Note: According to JDN96, the answer below may cause a memory leak by repeatedly destroying and recreating frames. However, I have not tested to verify this myself.

One way to switch frames in tkinter is to destroy the old frame then replace it with your new frame.

I have modified Bryan Oakley's answer to destroy the old frame before replacing it. As an added bonus, this eliminates the need for a container object and allows you to use any generic Frame class.

# Multi-frame tkinter application v2.3
import tkinter as tk

class SampleApp(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self._frame = None
        self.switch_frame(StartPage)

    def switch_frame(self, frame_class):
        """Destroys current frame and replaces it with a new one."""
        new_frame = frame_class(self)
        if self._frame is not None:
            self._frame.destroy()
        self._frame = new_frame
        self._frame.pack()

class StartPage(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        tk.Label(self, text="This is the start page").pack(side="top", fill="x", pady=10)
        tk.Button(self, text="Open page one",
                  command=lambda: master.switch_frame(PageOne)).pack()
        tk.Button(self, text="Open page two",
                  command=lambda: master.switch_frame(PageTwo)).pack()

class PageOne(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        tk.Label(self, text="This is page one").pack(side="top", fill="x", pady=10)
        tk.Button(self, text="Return to start page",
                  command=lambda: master.switch_frame(StartPage)).pack()

class PageTwo(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        tk.Label(self, text="This is page two").pack(side="top", fill="x", pady=10)
        tk.Button(self, text="Return to start page",
                  command=lambda: master.switch_frame(StartPage)).pack()

if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()

Start page Page one Page two

Explanation

switch_frame() works by accepting any Class object that implements Frame. The function then creates a new frame to replace the old one.

  • Deletes old _frame if it exists, then replaces it with the new frame.
  • Other frames added with .pack(), such as menubars, will be unaffected.
  • Can be used with any class that implements tkinter.Frame.
  • Window automatically resizes to fit new content

Version History

v2.3

- Pack buttons and labels as they are initialized

v2.2

- Initialize `_frame` as `None`.
- Check if `_frame` is `None` before calling `.destroy()`.

v2.1.1

- Remove type-hinting for backwards compatibility with Python 3.4.

v2.1

- Add type-hinting for `frame_class`.

v2.0

- Remove extraneous `container` frame.
    - Application now works with any generic `tkinter.frame` instance.
- Remove `controller` argument from frame classes.
    - Frame switching is now done with `master.switch_frame()`.

v1.6

- Check if frame attribute exists before destroying it.
- Use `switch_frame()` to set first frame.

v1.5

  - Revert 'Initialize new `_frame` after old `_frame` is destroyed'.
      - Initializing the frame before calling `.destroy()` results
        in a smoother visual transition.

v1.4

- Pack frames in `switch_frame()`.
- Initialize new `_frame` after old `_frame` is destroyed.
    - Remove `new_frame` variable.

v1.3

- Rename `parent` to `master` for consistency with base `Frame` class.

v1.2

- Remove `main()` function.

v1.1

- Rename `frame` to `_frame`.
    - Naming implies variable should be private.
- Create new frame before destroying old frame.

v1.0

- Initial version.
Stevoisiak
  • 23,794
  • 27
  • 122
  • 225
  • 1
    I get this weird bleed-over of frames when I try this method not sure if it can be replicated: https://imgur.com/a/njCsa – quantik Apr 03 '18 at 14:27
  • 2
    @quantik The bleed-over effect happens because the old buttons aren't being destroyed properly. Make sure you are attaching buttons directly to the frame class (usually `self`). – Stevoisiak Apr 04 '18 at 17:56
  • 1
    In hindsight, the name `switch_frame()` could be a bit misleading. Should I rename it to `replace_frame()`? – Stevoisiak May 22 '18 at 17:14
  • How can i switch the frames by keeping the class files in different files – Deepworks Sep 24 '18 at 11:40
  • 1
    @Deepworks I hadn't considered multiple class files. Unfortunately, multiple files may be prone to [circular dependencies](https://en.wikipedia.org/wiki/Circular_dependency). – Stevoisiak Sep 24 '18 at 18:35
  • 16
    i recommend removing the version history part of this answer. It's completely useless. If you want to see the history, stackoverflow provides a mechanism to do that. Most people who read this answer won't care about the history. – Bryan Oakley Nov 06 '18 at 13:21
  • 3
    Thank you for the version history. It is good to know that you updated and revised the answer over time. – Vasyl Vaskivskyi Feb 28 '19 at 23:12
  • I can't reproduce the "unintended side effect" when I run the code in @Bryan's answer—so don't understand that premise... – martineau Apr 27 '19 at 16:57
  • What if one was to try to call `lambda: master.show_frame(PageTwo)` from within a function? i.e. not as a direct command from a button but rather the result of a button press. Button runs a function in PageOne's Class, the function says "True", therefore the `lambda: show_frame(PageTwo)` is called and the page is supposed to switch. I've tried it and the page doesn't switch. any ideas? – Jguy Sep 24 '19 at 16:01
  • @JDN96 I’ve added a memory leak warning to my answer. Thanks for the heads up. – Stevoisiak Nov 08 '19 at 16:51
  • 2
    @StevoisiaksupportsMonica: The linked **memory leak** is related to `Perl/Tk subsystem`. Nowhere is commited this also happens using `Python`. – stovfl Dec 21 '19 at 15:38
  • @Jguy: This answer has no `master` nor `show_frame()` method, so for those reasons alone your comment makes no sense. Please delete it. – martineau Jan 03 '20 at 09:31
3

Perhaps a more intuitive solution would be to hide/unhide frames using the pack_forget method if you are using the pack geometry manager.

Here's a simple example.

import tkinter as tk


class App:
    def __init__(self, root=None):
        self.root = root
        self.frame = tk.Frame(self.root)
        self.frame.pack()
        tk.Label(self.frame, text='Main page').pack()
        tk.Button(self.frame, text='Go to Page 1',
                  command=self.make_page_1).pack()
        self.page_1 = Page_1(master=self.root, app=self)

    def main_page(self):
        self.frame.pack()

    def make_page_1(self):
        self.frame.pack_forget()
        self.page_1.start_page()


class Page_1:
    def __init__(self, master=None, app=None):
        self.master = master
        self.app = app
        self.frame = tk.Frame(self.master)
        tk.Label(self.frame, text='Page 1').pack()
        tk.Button(self.frame, text='Go back', command=self.go_back).pack()

    def start_page(self):
        self.frame.pack()

    def go_back(self):
        self.frame.pack_forget()
        self.app.main_page()


if __name__ == '__main__':
    root = tk.Tk()
    app = App(root)
    root.mainloop()

enter image description here

enter image description here

Chris Collett
  • 1,074
  • 10
  • 15