4

I have a problem stacking 'Pages' on top of each other in tkinter.

I have a main Frame that contains two sub frames which both contain different information. The first sub frame contains a Listbox and a couple buttons and is packed to the left in the main Frame. The 2nd frame is supposed to conain different 'Pages' (two for now) and have them fill up the entire frame. My issue is that both 'Pages' are displayed side by side instead of on top of each other.

import tkinter as tk


class Settings(tk.Tk):

    def __init__(self, master=None):
        tk.Tk.__init__(self, master)
        self.focus_force()
        self.grab_set()
        # set focus to settings window
        # Main window title
        self.title("Settings")


        # set up grid containers
        container_main = tk.Frame(self, width=500, height=700)
        container_main.pack(side='top', fill='both', expand=True)
        container_main.grid_rowconfigure(0, weight=1)
        container_main.grid_columnconfigure(0, weight=1)

        container_listbox = tk.Frame(container_main, bg='blue', width=200, height=700)
        container_listbox.pack(side='left', fill='both', expand=True)
        container_listbox.grid_rowconfigure(0, weight=1)
        container_listbox.grid_columnconfigure(0, weight=1)

        container_settings = tk.Frame(container_main, bg='red', width=300, height=700)
        container_settings.pack(side='right', fill='both', expand=True)
        container_settings.grid_rowconfigure(0, weight=1)
        container_settings.grid_columnconfigure(0, weight=1)

        # build settings pages
        self.frames = {}

        self.frames["Options"] = Options(parent=container_listbox, controller=self)
        self.frames["General"] = General(parent=container_settings, controller=self)
        self.frames["Future"] = Future(parent=container_settings, controller=self)

if I uncoment these two lines. I get an error saying I cannot use geometry manager grid inside.

        # self.frames["General"].grid(row=0, column=0, sticky='nsew')
        # self.frames["Future"].grid(row=0, column=0, sticky='nsew')

.

    def show_frame(self, page_name):
        frame = self.frames[page_name]
        frame.tkraise()


class Options(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(parent, text='List Box')
        label.grid(row=0, column=0, sticky='nsew', padx=1, pady=1)
        button1 = tk.Button(parent, text='General', command=lambda: controller.show_frame('General'))
        button2 = tk.Button(parent, text='Future', command=lambda: controller.show_frame('Future'))
        button1.grid(row=1, column=0, sticky='ew')
        button2.grid(row=2, column=0, sticky='ew')


class General(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(parent, text='General')
        label.pack(side='left', fill='both', expand=True, )
        print("Hi I'm General")

class Future(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(parent, text='Future')
        label.pack(side='left', fill='both', expand=True)
        print("Hi I'm Future")

app = Settings()
app.mainloop()

Both 'Pages' are initialized and displayed at the same time which makes sense. I just don't know how to make one rise over the other since frame.tkraise() supposed to be doing this but is not. I would also like to be able to do grid_forget() on the page or pages that are not on top to avoid potentially accidentally enter values into a hidden entrybox in the future.

EDIT: If I comment out the 'Future' page then the 'General' page will take up the whole frame space so with grid_forget() I would yield the same result. I just don't know where I would but grid_forget() and then also where would I re-configure or do a grid() call?

Ernie Peters
  • 537
  • 1
  • 6
  • 18

2 Answers2

3

My issue is that both 'Pages' are displayed side by side instead of on top of each other.

If you use pack() to place a frame on your root window and then use grid() inside of that frame then it will work but if you try to use pack() inside of a frame and then try to use grid() inside of that same frame it will fail.

The same goes for the root window and frames. If pack() a frame in the root window then you cannot use grid() to place anything into that same root window.

The problem with the grid() vs pack() issue was because the location where the class General and class Future was configuring the label widgets was the parent frame where pack() was being used. This prevented the use of grid() in that same parent frame to place the General and Future frames.

To fix this we change:

label = tk.Label(parent, text='General')

and

label = tk.Label(parent, text='Future')

to:

label = tk.Label(self, text='General')

and

label = tk.Label(self, text='Future')

the above was the only fix needed for this to work properly.

import tkinter as tk


class Settings(tk.Tk):

    def __init__(self, master=None):
        tk.Tk.__init__(self, master)
        self.focus_force()
        self.grab_set()
        # set focus to settings window
        # Main window title
        self.title("Settings")

        container_main = tk.Frame(self, width=500, height=700)
        container_main.pack(side='top', fill='both', expand=True)
        container_main.grid_rowconfigure(0, weight=1)
        container_main.grid_columnconfigure(0, weight=1)

        container_listbox = tk.Frame(container_main, bg='blue', width=200, height=700)
        container_listbox.pack(side='left', fill='both', expand=True)
        container_listbox.grid_rowconfigure(0, weight=1)
        container_listbox.grid_columnconfigure(0, weight=1)

        container_settings = tk.Frame(container_main, bg='red', width=300, height=700)
        container_settings.pack(side='right', fill='both', expand=True)
        container_settings.grid_rowconfigure(0, weight=1)
        container_settings.grid_columnconfigure(0, weight=1)

        self.frames = {}

        self.frames["Options"] = Options(parent=container_listbox, controller=self)
        self.frames["General"] = General(parent=container_settings, controller=self)
        self.frames["Future"] = Future(parent=container_settings, controller=self)   


        self.frames["General"].grid(row=0, column=0, sticky='nsew')
        self.frames["Future"].grid(row=0, column=0, sticky='nsew')

    def show_frame(self, page_name):
        frame = self.frames[page_name]
        frame.tkraise()

class Options(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(parent, text='List Box')
        label.grid(row=0, column=0, sticky='nsew', padx=1, pady=1)
        button1 = tk.Button(parent, text='General', command=lambda: controller.show_frame('General'))
        button2 = tk.Button(parent, text='Future', command=lambda: controller.show_frame('Future'))
        button1.grid(row=1, column=0, sticky='ew')
        button2.grid(row=2, column=0, sticky='ew')


class General(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text='General')
        label.pack(side='left', fill='both', expand=True)
        print("Hi I'm General")

class Future(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text='Future')
        label.pack(side='left', fill='both', expand=True)
        print("Hi I'm Future")

app = Settings()
app.mainloop()
Mike - SMT
  • 14,784
  • 4
  • 35
  • 79
  • that solves the `cannot use geometry manager grid inside` issue. I still need to only display one 'Page' at a time such as in [Bryan Oakley's](https://stackoverflow.com/a/7557028/7703610) example minus the for loop. – Ernie Peters Jul 26 '17 at 15:15
  • Your first statement is slightly misleading. You can use both pack and grid in a window, not just with widgets that share a common parent. Your statement almost makes it sound like if you use pack anywhere in a window then you can't use grid anywhere, which is false. – Bryan Oakley Jul 26 '17 at 15:24
  • It probably how you are using pack on the frames. – Mike - SMT Jul 26 '17 at 15:25
  • @BryanOakley I guess my wording is off. Correct me if I am wrong buy if I use `pack()` inside of say frame1 and then try to use `grid()` in frame1 this should cause an error. I do know that you can use pack() on `frame1 = Frame(root).pack()` and then do something like `entry = Entry(frame1).grid(row=0, column=0) and it will work fine. – Mike - SMT Jul 26 '17 at 15:28
  • @BryanOakley: I have updated my answer to me more clear on the geometry manager portion. – Mike - SMT Jul 26 '17 at 15:31
  • I'm using `container_main` as the root frame and then pack 'container_listbox` and `container_settings` inside of `container_main` . Then I populate the `container_settings` with the pages and use `container_settings` as the parent for the page widgets. The `container_listbox` is behaving just like I want it. I use grid to place both pages into `container_settings` but they behave as if though I used pack. Does this make sense at all? – Ernie Peters Jul 26 '17 at 15:54
  • @ErniePeters: I am working on it now. I am not sure why each frame is not being raised when the buttons are clicked. I will update my answer when I figure it out. – Mike - SMT Jul 26 '17 at 15:55
  • @BryanOakley: Do you know why `frame.tkraise()` is not working here. When I look at the code it appears to me that it should work fine but it does not actually raise the frame. – Mike - SMT Jul 26 '17 at 16:01
  • In my actual application. This will become a `Topframe` window. As far as the `frame.tkraise()` goes, It is not throwing any errors and/or exceptions but I noticed that when typing the code in PyCharm, it did not actually auto-complete as if it did not inherit the `tkraise()` attribute. Normally typing tk.whatever it would pull up a list of available suggestions. – Ernie Peters Jul 26 '17 at 16:05
  • I've updated my answer. Short version: `tkraise()` is working, but you're calling it on the wrong thing. – Bryan Oakley Jul 26 '17 at 16:19
  • @SierraMountainTech thanks I appreciate it. I knew I was overlooking something obvious. – Ernie Peters Jul 26 '17 at 17:00
  • @ErniePeters: I just updated my answer again.. sorry for that. But this new answer is the correct answer to the issue you were having lol. This was a mess for sure but I got it figured out – Mike - SMT Jul 26 '17 at 17:02
  • @BryanOakley: Well I managed to figure out the problem. The entire time it was just where the labels were being placed in the General and Future classes. – Mike - SMT Jul 26 '17 at 17:03
  • @SierraMountainTech no worries. I've learned so much from you and Bryan over the last couple of weeks. I also like flevinkelming incorporation of the `pack_forget()` so it'll be a tough decision on which answer to pick lol – Ernie Peters Jul 26 '17 at 17:05
  • @ErniePeters Its a learning process for me as well. This answer is a good example of how my knowledge of something is challenged and I have to rethink things to learn more about it. – Mike - SMT Jul 26 '17 at 17:07
  • @BryanOakley the labels dont go to parent. That was the problem to begin with. they now go to `self`. `label = tk.Label(self, text='General')` – Mike - SMT Jul 26 '17 at 17:09
  • @BryanOakley why did you delete your comment and down vote my answer? I am confused as to what it is that is wrong with it. The labels are now being placed on `self` and from my understanding that should be the frame created by the class it is a part of correct? – Mike - SMT Jul 26 '17 at 17:16
  • I downvoted by accident. If you edit your answer I can fix it. I deleted my answer because I didn't think it was useful as written and I don't have time to rewrite it. I think your answer would be better if you removed all of the "Edit" sections, and just included the relevant information. – Bryan Oakley Jul 26 '17 at 17:19
  • @BryanOakley: Ok. You had me worried for a second. I thought I offended you or something. Removed the EDITS. – Mike - SMT Jul 26 '17 at 17:19
3

I have taken the liberty of restructuring your application logic to convey how it might be easier to solve your problem - I've included some additional information as well.

The issue with your current implementation in it's ability to hide and show your pages, is in your command keyword assignment for your buttons. In the implementation I provided, the Options class is where all the magic happens - specifically, the show_general, and show_future methods. Simply, when the general button is clicked, I invoke pack_forget() on the future_page widget, pack(...) on the general_page widget, and vice versa when I want to show the future page. (This can implementation can easily be scaled, but I'll leave that up to you.)

Additionally, I structured the application logic in a way that modularizes the code, so at each step I could test it and see where I'm at, e.g. I started in main() creating each container, and making sure everything was laid out correctly, (I like to give each container a different color, making it easier to visualize spacing, and such) before moving on to the Settings and Options frames where I would place the widgets, and write the code needed for the GUI to function as intended.

I won't explain every line, but I'm sure at this point you can see what I'm getting at. I recommend reading through the code (starting with main()), and figuring out how, and why I wrote this in the manner that I did. (I wrote this to function as I believe you intended, as well as to offer some pointers here and there - It is not meant to be perfect.)

import tkinter as tk

class Options(tk.Frame):

    def __init__(self, master):

        tk.Frame.__init__(self, master, bg='#FFFFFF', relief='ridge', bd=1)

        # Grab the 'Settings' tk.Frame object
        settings_frame = self.master.master.winfo_children()[1].winfo_children()[0]
        self.settings = settings_frame

        self.options()


    def options(self):
        self.label = tk.Label(self, text='List Box')
        self.button1 = tk.Button(self, text='General', command=self.show_general)
        self.button2 = tk.Button(self, text='Future', command=self.show_future)

        self.label.grid(row=0, sticky='nsew', padx=1, pady=1)  # column is set to 0 by default
        self.button1.grid(row=1, sticky='nsew', padx=1, pady=1)
        self.button2.grid(row=2, sticky='nsew', padx=1, pady=1)


    def show_general(self):
        self.settings.future_page.pack_forget()
        self.settings.general_page.pack(fill='both', expand=True)


    def show_future(self):
        self.settings.general_page.pack_forget()
        self.settings.future_page.pack(fill='both', expand=True)


class Settings(tk.Frame):

    def __init__(self, master):

        tk.Frame.__init__(self, master, bg='#FFFFFF', relief='ridge', bd=1)

        self.pages()


    def pages(self):
        self.general_page = tk.Label(self, fg='#FFFFFF', bg='#FF0000',
            relief='ridge', bd=1, text="Hi, I'm General.")
        self.future_page = tk.Label(self, fg='#FFFFFF', bg='#0000FF',
            relief='ridge', bd=1, text="Hi, I'm Future.")

        self.general_page.pack(fill='both', expand=True)


def main():
    root = tk.Tk()
    root.title('Settings')
    root.configure(bg='#DDDDDD', width=500, height=500)
    root.resizable(width=False, height=False)

    main_container = tk.Frame(root, bg='#FFFFFF', width=500, height=500)
    main_container.pack(fill='both', padx=20, pady=20)  # Add some padding to see container difference
    main_container.pack_propagate(False)  # Avoid sizing based on widget contents

    listbox_left = tk.Frame(main_container, bg='#4285F4', width=235)  # Take 15 from both sides for padding
    settings_right = tk.Frame(main_container, bg='#272727', width=235)

    listbox_left.pack(side='left', fill='y', padx=(10, 0), pady=10)
    listbox_left.pack_propagate(False)
    settings_right.pack(side='right', fill='y', padx=(0, 10), pady=10)
    settings_right.pack_propagate(False)

    settings = Settings(settings_right)  # Must be instantiated before Options
    options = Options(listbox_left)

    settings.pack(fill='both', expand=True, padx=2, pady=2)
    options.pack(fill='both', expand=True, padx=2, pady=2)

    root.mainloop()


if __name__ == '__main__':
    main()

I hope this helps!

flevinkelming
  • 690
  • 7
  • 8
  • _"You're on the right track, however there needs to be some way for both widgets to be invoked, in order for one to be shown, and one to be hidden. "_ - this is not true. If two widgets are stacked, you simply need to raise one above the other. – Bryan Oakley Jul 26 '17 at 16:18
  • @flevinkelming, thanks so much. I approach a problem the same way you do with the colors and modularizing. I simply got lost on the way. Incorporating `pack_forget()` is also what I was looking for so thanks for a simple yet thorough example. – Ernie Peters Jul 26 '17 at 17:03
  • Hey Flevinkelming. Take a look at my answer. You might be surprised how simple the actual solution turned out to be. It took me a few rounds of testing but I got it figured out. – Mike - SMT Jul 26 '17 at 17:05
  • @ErniePeters glad you were able to figure it out. I simply prefer `pack`, but there's also `grid_remove()` and `grid_forget()` when using `grid`. @SierraMountainTech nice! – flevinkelming Jul 26 '17 at 17:13
  • @flevinkelming Ultimately I can only pick one answer as accepted bu I really appreciate your answer as well. – Ernie Peters Jul 26 '17 at 17:27
  • I think @SierraMountainTech 's answer is more direct in terms of answering the question - my answer was given to offer pointers, and show how the solution could be achieved alternatively. – flevinkelming Jul 26 '17 at 17:38