I'm struggling with tkraise not hiding the 'bottom' frame in my app.
I have two frames, one contains a Listbox and is packed to the left and the other will display options for each item in the listbox and is packed to the right.
My problem is that I can see the Future page when I select General and vise versa. I copied and modified it from my working main app but I don't know what I did wrong to break it for this one.
# All settings windows and forms labels are built in here
import tkinter as tk
# from main import WinSize
from tkinter import Listbox, END, ttk
class Settings(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
# create frame for listbox and parameters area
self.list_container = ttk.Frame(self, relief='sunken')
self.list_container.pack(side='left', fill='y', expand=False)
self.param_container = ttk.Frame(self)
self.param_container.pack(side='top', fill='both', expand=True)
self.options_list = Listbox(self.list_container, selectmode='single')
for choice in ['General', 'Future']:
self.options_list.insert(END, choice)
self.okbutton = ttk.Button(self.param_container, text="OK", command= self.destroy)
self.okbutton.grid_rowconfigure(0, weight=1)
self.okbutton.grid_columnconfigure(0, weight=1)
self.okbutton.grid(row=2, column=2, sticky='nsew')
# Grid layout for Settings window
self.options_list.grid(row=0, column=0, sticky='nsew')
self.list_container.grid_rowconfigure(1, weight=1)
self.list_container.grid_columnconfigure(1, weight=1)
self.param_container.grid_rowconfigure(1, weight=1)
self.param_container.grid_columnconfigure(1, weight=1)
# create empty TUPLE for future frames
self.frames = {}
# generate calls for frames
for F in (General, Future):
self.page_name = F.__name__
self.frame = F(parent=self.param_container, controller=self)
self.frames[self.page_name] = self.frame
self.options_list.select_set(0)
self.options_list.bind("<<ListboxSelect>>", self.onselect)
self.options_list.event_generate("<<ListboxSelect>>")
# grab value of listbox selection and call show_frame
def onselect(self, event):
self.widget = event.widget
self.value = self.widget.get(self.widget.curselection())
print(self.value)
self.show_frame(self.value)
# show corresponding frame based on listbox selection
def show_frame(self, page_name):
# show a frame for the given page name
self.frame = self.frames[page_name]
self.frame.grid(row=0, column=1, sticky='nsew')
self.frame.tkraise()
print("Show Frame")
class General(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.optiontitle = ttk.Label(parent, text='General')
self.optiontitle.grid(row=0, column=0, sticky='nsew')
self.dirlabel = ttk.Label(parent, text='Default Save Directory')
self.dirlabel.grid(row=1, column=1, sticky='s')
class Future(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
test1 = ttk.Label(self, text='Future')
test1.pack()
app=Settings()
app.mainloop()
I want to say it may be something to do with my grid layout but it doesn't make sense since the two 'pages' are not coupled (or supposed to be) with each other.
I solved this issue through another problem that I was able to work out with some help from others. Refer to this → Frames not stacking on top of each other in tkinter question as it has two great answers which easily allow to incorporate grid_forget() and/or pack_forget() as suggested by #jasonharper.
You aren't doing anything to actually hide the other page; you're just layering the new page on top of it, which isn't going to look right unless they occupy exactly the same screen area.
Even if they did, this still isn't a workable approach, since the widgets in the hidden page are still active. In particular, if it had any Entry or Text fields, they could still have (or gain) keyboard focus, so anything the user types might mysteriously end up in a field they can't even see at the moment.
You should call .grid_forget() on the previous page when showing the new one. Or, perhaps easier, call .grid_forget() on all pages before calling .grid() on the new one (it doesn't hurt to call this on a widget that isn't currently shown).
Related
I just started using python and I am trying to make a simple gui that consists of 3 frames. One on the left, one on the right and one in the middle. Later I'd like to add buttons and stuff to those frames but for now that is all. I want the left frame to disappear or appear again if I press the escape key. To do this I have written the following code:
from tkinter import Tk, Button, Label, Frame
class Main:
def __init__(self):
self.root = Tk()
self.init_gui()
def init_gui(self):
self.root.title("Gui Testing")
self.root.minsize(900,600)
self.root.bind("<Escape>", self.toggle_left_menu)
self.root.grid_columnconfigure(0, minsize=200)
self.root.grid_columnconfigure(1, weight=1)
self.root.grid_columnconfigure(2, minsize=250)
self.root.grid_rowconfigure(0, weight=1)
# main 3 panels
self.left_menu_active = True
self.left_menu = Frame(self.root, bg="#333")
self.left_menu.grid(row=0, column=0, sticky="nsew")
self.center = Frame(self.root, bg="white")
self.center.grid(row=0, column=1, sticky="nsew")
self.right_menu = Frame(self.root, bg="#888")
self.right_menu.grid(row=0, column=2, sticky="nsew")
self.toggle_left_menu()
def toggle_left_menu(self, event=None):
if self.left_menu_active == True:
self.left_menu_active = False
self.root.grid_columnconfigure(0, minsize=0)
self.left_menu.grid_forget()
else:
self.left_menu_active = True
self.root.grid_columnconfigure(0, minsize=200)
self.left_menu.grid(row=0, column=0, sticky="nsew")
def start(self):
self.root.mainloop()
Main().start()
The problem is that when I press escape, nothing happens. However, when I then move the window by clicking on it and dragging it, it updates all of a sudden and it shows the window the way I want it. So the code seems to be working but the window isn't updating for some reason.
I don't know what I can do about that. I found out that it does update the grid positions of the left and the center frame, but the grid_configure() doesn't seem to update without moving the window.
Is there a way to update the frame or to achieve the frame toggling in some other way?
Edit:
The problem has been solved by adding a button to each frame. Now the frames are not empty anymore it seems works. I also edited the toggle_left_menu() function a bit. This is what I changed:
Added Buttons:
self.test_button1 = Button(self.left_menu, text="left", padx=10, pady=5)
self.test_button1.grid(row=0, column=0)
self.test_button2 = Button(self.center, text="center", padx=10, pady=5)
self.test_button2.grid(row=0, column=0)
self.test_button3 = Button(self.right_menu, text="right", padx=10, pady=5)
self.test_button3.grid(row=0, column=0)
Edited toggle_left_menu():
def toggle_left_menu(self, event=None):
if self.left_menu.winfo_viewable():
self.root.grid_columnconfigure(0, minsize=0)
self.left_menu.grid_remove()
else:
self.root.grid_columnconfigure(0, minsize=200)
self.left_menu.grid()
This worked for me, thanks!
Extending Bryan Oakley's example ~ you have to toggle the minsize, as well. As an aside, I classed out all of your gui and made Main the root. All that self.root.this and self.root.that is unnecessary this way. Also, you would have had to do this anyway, unless you intended to dump your entire gui contents into your init_gui method. If your app is large that would be a nightmare to keep track of. As an added bonus, I made the whole toggle_menu method dynamic so it can toggle either menu. You can change the key-bindings, to whatever. I used Escape then l for left_menu and Escape then r for right_menu.
from tkinter import Tk, Button, Label, Frame
class LeftMenu(Frame):
#property
def minsize(self):
return 200
def __init__(self, master, row=0, column=0, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, sticky='nswe')
class RightMenu(Frame):
#property
def minsize(self):
return 250
def __init__(self, master, row=0, column=0, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, sticky='nswe')
class Center(Frame):
def __init__(self, master, row=0, column=0, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, sticky='nswe')
class Main(Tk):
def __init__(self):
Tk.__init__(self)
self.bind("<Escape><l>", self.toggle_menu)
self.bind("<Escape><r>", self.toggle_menu)
self.grid_columnconfigure(0, minsize=200)
self.grid_columnconfigure(1, weight=1)
self.grid_columnconfigure(2, minsize=250)
self.grid_rowconfigure(0, weight=1)
# main 3 panels
self.left_menu = LeftMenu(self, 0, 0, bg="#333")
self.center = Center(self, 0, 1, bg="white")
self.right_menu = RightMenu(self, 0, 2, bg="#888")
self.toggle_menu(menu=self.left_menu)
def toggle_menu(self, event=None, menu=None):
if event and event.char in 'lr':
menu = self.left_menu if event.char == 'l' else self.right_menu
if menu:
if menu.winfo_viewable():
self.grid_columnconfigure(menu.grid_info()['column'], minsize=0)
menu.grid_remove()
else:
menu.grid()
self.grid_columnconfigure(menu.grid_info()['column'], minsize=menu.minsize)
self.after_idle(self.event_generate, '<Configure>')
if __name__ == "__main__":
root = Main()
root.title("Gui Testing")
root.minsize(900,600)
root.mainloop()
Part of the problem is that minsize only affects the minimum size. If the left frame is visible and is more than zero pixels wide, setting the minsize to zero isn't going to make the frame smaller. So, one step is to remove the minsize=200 option for column 0.
Since you are using grid, the best way to hide or show a frame is to use grid_remove to remove the widget, and then grid to restore it. grid_remove will remove the widget from the window but remember all of its settings. When you subsequently call .grid(), all of the previous settings will be used.
You can also just check if the window is visible or not without having to manage a boolean flag since your function is a toggle. That simplifies the code a bit.
Also, I think there's a bug on some versions of tk (the library upon which tkinter is built) that prevents the window from refreshing in this specific type of situation. What works for me is to synthetically generate a <Configure> event on the root window.
Rolling all of that together, this version of your toggle function works for me on OSX without any other modifications to your code.
def toggle_left_menu(self, event=None):
if self.left_menu.winfo_viewable():
self.left_menu.grid_remove()
self.root.after_idle(self.root.event_generate, '<Configure>')
else:
self.left_menu.grid()
self.root.after_idle(self.root.event_generate, '<Configure>')
Please help anyone
You can read all details as an inline comment.
I have created three classes LeftFrame, RightFrame, DynamicWindow
In DynamicWindow I am inheriting RightFrame
Step 1
LeftFrame, in column 0 with minsize 350
Step 2
RightFrame, in column 1 with weight 1, capturing all available space
Step 3
DynamicWindow, ingeriting RightFrame , Here is main problem, Please read the code.
import tkinter as tk
from win32api import GetMonitorInfo, MonitorFromPoint
root = tk.Tk()
monitor_info = GetMonitorInfo(MonitorFromPoint((0, 0)))['Work']
root.geometry(f'{monitor_info[2]}x{monitor_info[3]}')
root.state('zoomed')
root.columnconfigure(0, minsize=350) # Width of left frame
root.columnconfigure(1, weight=1) # All available space for right frame
root.rowconfigure(0, weight=1) # Full screen height for both frame
class LeftFrame(tk.Frame):
"""
Left Frame
"""
def __init__(self, container):
super().__init__(container)
self.config(bg='red')
self.grid(row=0, column=0, sticky='nsew')
class RightFrame(tk.Frame):
"""
Right Frame:
Divided into three section head frame, middle frame and bottom frame
head frame contains button
middle frame contains dynamically changeable frame. Here I am facing problem,
frame not able to take actual size according to weight and minsize that I given.
"""
def __init__(self, container):
super().__init__(container)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, minsize=30)
self.rowconfigure(1, weight=1)
self.rowconfigure(2, minsize=30)
self.config(bg='green')
self.grid(row=0, column=1, sticky='nsew')
self.head_frame = tk.Frame(self, bg='orange')
self.head_frame.grid(row=0, column=0, sticky='nsew')
self.head_frame.rowconfigure(0, weight=1)
self.first_screen_button = tk.Button(self.head_frame, text='First Screen')
self.first_screen_button.grid(row=0, column=0, sticky='nsew', ipadx=20)
self.second_screen_button = tk.Button(self.head_frame, text='Second Screen')
self.second_screen_button.grid(row=0, column=1, sticky='nsew', ipadx=20)
self.middle_frame = tk.Frame(self, bg='green')
self.middle_frame.grid(row=1, column=0, sticky='nsew')
self.bottom_frame = tk.Frame(self, bg='orange')
self.bottom_frame.grid(row=2, column=0, sticky='nsew')
class DynamicWindow(RightFrame):
def __init__(self, container):
super().__init__(container)
self.first_screen_button.config(command=self.first_screen)
self.second_screen_button.config(command=self.second_screen)
self.first_screen() # I am calling this here becuase on first click on first screen button
# window don't appear. You can check by comment this code.
# Can anyone please tell me why first screen not appear on first click.
def first_screen(self):
"""
First screen that I want to appear when I click on button
It has two frame
"""
for widget in self.middle_frame.winfo_children():
# Want to destroy all available widget in middle frame
widget.destroy()
self.middle_frame.columnconfigure(0, weight=1)
# Configuring size and weight but this is not working properly
self.middle_frame.columnconfigure(1, minsize=30)
self.middle_frame.rowconfigure(0, weight=1)
self.middle_frame.rowconfigure(1, weight=0)
main_chart_window = tk.Frame(self.middle_frame, bg='#4d4d4d')
main_chart_window.grid(row=0, column=0, sticky='nsew')
toolbar = tk.Frame(self.middle_frame, bg='red')
toolbar.grid(row=0, column=1, sticky='nsew')
def second_screen(self):
"""
This is not working in proper way
I am not able to reconfigure weight of middle frame
I want this window in full screen in middle frame
Here you will notice column 1 taking minsize 30, can anyone solve this
"""
for widget in self.middle_frame.winfo_children():
widget.destroy()
self.middle_frame.columnconfigure(0, weight=1)
self.middle_frame.rowconfigure(0, weight=1)
second_screen_window = tk.Frame(self.middle_frame, bg='purple')
second_screen_window.grid(row=0, column=0, sticky='nsew')
left_frame = LeftFrame(root)
dynamic_window = DynamicWindow(root)
root.mainloop()
I made it all work for you. I lost interest in fighting with your code/method so, I completely rewrote the code from scratch and devised a different method. All of the issues that you highlighted have been resolved. The structure of my code should be much easier to work with. The main issue is that you were destroying children, but you weren't destroying the column and/or row that the children were in. You basically can't. Using grid_forget() even in conjunction with destroy() or grid_remove() doesn't seem to remove the grid cell.
changes:
Every major widget has been separated into it's own class
Names have been changed to reflect the actual purpose of each widget (to the best of my ability based on your example)
We swap 'main display' widgets by removing/re-instating the entire widget ~ instead of destroying/recreating all of it's children
a lambda is used in the button command to pass the desired 'main display' to the method that does the swapping
we never use super() to instantiate a class. We specifically refer to the super by classname
all args and kwargs are maintained, so we can treat our custom widgets like their super
we only import exactly what we need (my preference)
The comments should tell you the rest, but if there is confusion, point it out to me in the comment section and I will respond with a more detailed explanation.
widgets.py
from tkinter import Frame, Button
from typing import List, Dict, Callable
from dataclasses import dataclass
#a simple "typedef" for storing menu button data
#dataclass
class MenuData_t:
func:Callable #method the command lambda will call
buttons:List[Dict] #Button(**kwargs)
targets:List[Frame] #'main display' to switch to
griddata:List[Dict] #.grid(**kwargs)
'''
this replaces your 'head_frame'
it also provides an interface to concoct all of the buttons that will swap 'main displays'
if you need other types of buttons you will need to manually create them in __init__
considerations have been made in init_displayswap_menu for existing buttons
'''
class MenuFrame(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
def init_displayswap_menu(self, md:MenuData_t):
c = len(self.winfo_children())
for i, (b, t, g) in enumerate(zip(md.buttons, md.targets, md.griddata)):
self.__dict__[f'swap_btn{i+1}'] = Button(self, command=lambda m=t: md.func(m), **b)
self.__dict__[f'swap_btn{i+1}'].grid(row=0, column=i+c, **g)
#this replaces your "bottom_frame"
class Footer(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
#this replaces your "first_screen"
class PrimaryFrame(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
self.chart = Frame(self, bg='#4d4d4d')
self.chart.grid(row=0, column=0, sticky='nswe')
self.toolbar = Frame(self, bg='red')
self.toolbar.grid(row=0, column=1, sticky='nswe')
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, minsize=30)
self.grid_rowconfigure(0, weight=1)
#this replaces your "second_screen"
class SecondaryFrame(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
#this replaces your "LeftFrame"
class Sidebar(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
#this replaces your "RightFrame" AND "DynamicWindow"
class MainFrame(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
##INSTANTIATE
#menu
self.menu = MenuFrame(self, bg='orange')
self.menu.grid(row=0, column=0, sticky='nsew')
#main display
self.current = None #for storing currently used 'main display'
self.primary = PrimaryFrame(self, bg='green')
self.secondary = SecondaryFrame(self, bg='purple')
#footer
self.footer = Footer(self, bg='orange')
self.footer.grid(row=2, column=0, sticky='nsew')
##UTILIZE
#concoct main display swap menu
'''
append accordingly to the 3 lists to create more buttons that will switch frames
done this way so button creation can remain in MenuFrame but use remote data
row, column and command are managed in MenuFrame
'''
self.menu.init_displayswap_menu(MenuData_t(
self.main_display, #method the command lambda will call
[{'text':'Primary'}, #Button(**kwargs)
{'text':'Secondary'},
],
[self.primary, #'main display' to switch to
self.secondary,
],
[{'sticky':'nswe','ipadx':20}, #.grid(**kwargs)
{'sticky':'nswe','ipadx':20},
]
))
#init main display
'''
I could have called main_display directly but this illustrates 2 things
1: how to virtually click a button
2: how to access the buttons that MenuFrame created in it's __dict__
'''
self.menu.swap_btn1.invoke()
#configure grid
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, minsize=30)
self.grid_rowconfigure(1, weight=1)
self.grid_rowconfigure(2, minsize=30)
#replaces your 'first_screen' AND 'second_screen' methods
def main_display(self, frame):
if self.current is not frame: #only swap if we aren't requesting the current 'main display'
if self.current:
self.current.grid_remove() #remove current from the grid, instead of destroy
self.current = frame #set new current and add it to the grid
self.current.grid(row=1, column=0, sticky='nsew')
main.py
from win32api import GetMonitorInfo, MonitorFromPoint
from widgets import Sidebar, MainFrame
from tkinter import Tk
#This is your "root"
class Application(Tk):
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
Sidebar(self, bg='red').grid(row=0, column=0, sticky='nswe')
MainFrame(self, bg='black').grid(row=0, column=1, sticky='nswe')
#configure grid
self.grid_columnconfigure(0, minsize=350)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
#kick off the entire app with proper PEP8
if __name__ == '__main__':
monitor_info = GetMonitorInfo(MonitorFromPoint((0, 0)))['Work']
app = Application()
app.title("Manish Pushpam's Bad-Ass Application")
app.geometry(f'{monitor_info[2]}x{monitor_info[3]}')
app.minsize(800, 600)
app.mainloop()
I want the buttons to be on opposite sides of the screen, but I'm unsure as to why they keep positioning themselves in the middle. The code is set up like this because I plan on having multiple overlays that I switch between with the show_frame method. The class InputWindow is one of those overlays. When I created a basic script with only window and buttons, I was able to get the buttons positioned properly, but I'm unsure as to what I'm doing incorrectly here that's preventing me from distancing the buttons properly.
import tkinter as tk
class GuiController(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 = InputWindow(container, self)
self.frames[InputWindow] = frame
frame.pack()
self.show_frame(InputWindow)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class InputWindow(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
controller.geometry("650x500")
button_defaultGame = tk.Button(self, text = "Default Game")
button_defaultGame.grid(row = 0, column = 0, sticky = "W")
button_test = tk.Button(self, text = "Test")
button_test.grid(row = 0, column = 1, sticky = "E")
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=2)
app = GuiController()
app.mainloop()
When struggling with layout problems it helps to give your frames distinctive colors. Otherwise it can be difficult to see where one frame ends and another begins.
For example, give the container a color like this:
container = tk.Frame(self, bg="bisque")
Next, give your InputWindow a different color like this:
class InputWindow(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent, bg="pink")
When you run the code you'll see something like this:
This immediately makes it clear that InputWindow is not filling the container window. Looking through your code we can see that you're doing frame.pack() to add the instance of InputWindow to container.
Instead, you need to request that InputWindow fills the container window. You do that by changing your call to pack to look like this:
frame.pack(side="top", fill="both", expand=True)
Now we can see that the instance of InputWindow fills the container, and your buttons do indeed sit on the edges of the window.
I'm trying to replicate an answer to my own accord from this answer/question here: Switch between two frames in tkinter
As you can see from the answer above, tehy instantiate multiple sub-classes of the Frame widget, and when we want to switch pages, we click a button and that class a method from our base class.
However, wouldn't creating multiple 'pages' methods under a 'Pages' class, be a lot cleaner and make sense? I'm not sure what to believe, and I would love clarification as to how I should be tackling this project, and why using classes or instanced methods would be better?
I've added my comments into the code below for lines I don't quite understand and I'm hoping I can gain some knowledge from the world of StackOverflow.
import Tkinter as tk
LARGE_FONT = ("Verdana", 12)
class Main(tk.Tk):
def __init__(self, *args, **kwargs):
########### are we calling the Tk class from tkinter and passing in our 'Main' class?
tk.Tk.__init__(self, *args, **kwargs)
# why not self.container?
container = tk.Frame(self)
# again, should I be using self.page here instead?
page = Pages(parent=container, controller=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 ('page.page_one()', 'page.page_two()'):
# what exactly does __name__ do? And how can I replicate this with my derived classes instanced methods?
page_name = F#F.__name__
frame = page#(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=0, column=0, sticky='nsew')
self.show_frame("page.page_one()")
def show_frame(self, page_name):
frame = self.frames[page_name]
frame.tkraise()
class Pages(tk.Frame):
# i could just use *args, **kwargs here couldn't I?
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.page_one(controller)
# here I have my instance methods inside my derived 'Pages' class
# isn't this much cleaner, having multiple instance methods inside a class?
# or I should be separating each page into it's own instanced class?
def page_one(self, controller):
label = tk.Label(self, text='show_firmware_page', font=LARGE_FONT)
label.pack(pady=10, padx=10)
# how can I switch pages using the next button?
next_btn = tk.Button(self, text='Next', command=lambda: controller.show_frame(self.page_two(controller)))
quit_btn = tk.Button(self, text='Quit', command=lambda: controller.show_frame())
quit_btn.pack()
next_btn.pack()
def page_two(self, controller):
label = tk.Label(self, text='second_page', font=LARGE_FONT)
label.pack(pady=10, padx=10)
# how can I switch pages using the next button?
next_btn = tk.Button(self, text='Next', command=lambda: Pages.show_frame("page_one"))
quit_btn = tk.Button(self, text='Quit', command=lambda: Pages.show_frame())
quit_btn.pack()
next_btn.pack()
app = Main()
app.mainloop()
Basically, my current push is to try and use methods within my class in order to define my pages and switch between them. I'm currently having some trouble, and upon taking a look at other's answers, a lot of them have the idea that each class instantiates a Frame, in which we call a method to switch between those instances.
Let me know your thoughts on this process to get me up to speed about how I should be tackling this project.
Many thanks in advance for your help - I really want to get this OOP stuff down.
You could theoretically make methods in a single class reconfigure the UI the way you want, but it's probably not going to be easy.
Your current code can't work because there's no simple way for one of your methods to clean up the work done by another previous method (e.g. by getting rid of the label and buttons it created).
The original answer you linked to avoided that issue by having all the widgets created at the start of the program (not only when they're about to be displayed). Only one page is displayed at a time though, since they're all Frames that have been configured to display in the same location. The Frame that is on top hides the others and prevents their widgets from doing anything. By moving a different frame on top, you can switch between the pages.
If we ignore those widget display issues, you could make your main class call the methods you've written:
class Main(tk.Tk):
def __init__(self, *args, **kwargs):
container = tk.Frame(self) # I'm not sure a separate container is necessary
container.pack(side="top", fill="both", expand=True) # since we're only putting
container.grid_rowconfigure(0, weight=1) # one other frame inside it
container.grid_columnconfigure(0, weight=1)
page = Pages(parent=container, controller=self)
page.grid(row=0, column=0, sticky='nsew') # moved up from below
self.frames = {}
for F in (page.page_one, page.page_two): # no quotes or parentheses
page_name = F.__name__ # the names are the strings "page_one" and "page_two"
self.frames[page_name] = F
self.show_frame("page_one")
def show_frame(self, page_name):
method = self.frames[page_name]
method() # call the method we are switching to
class Pages(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
def page_one(self):
# do something to clean up previous pages here?
label = tk.Label(self, text='show_firmware_page', font=LARGE_FONT)
label.pack(pady=10, padx=10)
next_btn = tk.Button(self, text='Next',
command=lambda: self.controller.show_frame("page_two")
next_btn.pack()
def page_two(self):
label = tk.Label(self, text='second_page', font=LARGE_FONT)
label.pack(pady=10, padx=10)
next_btn = tk.Button(self, text='Next',
command=lambda: self.controller.show_frame("page_one"))
next_btn.pack()
This will work (for some definition of "work"). I removed the quit buttons because I'm not sure exactly what the best way to handle them is (you probably don't want them to be calling show_frame).
Note that I'm by no means an expert at TKinter, so it's entirely possible that there's some easy way to remove the widgets from the previous page when you've moved on to the next one. I just don't know how.
I have a small python program to build a GUI. I'm trying to use a text widget to create an easily scrollable window that contains vertically stacked frames. A frame is created on button press and added to the bottom of the text widget. This works fine; however, I'm struggling to get these frames to stretch to fill the text box horizontally.
import Tkinter as tk
class NewEntry(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
#self.pack(fill="x", expand=True) #possible error source
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0,weight=1)
textField = tk.Entry(self)
textField.grid(row=1, column=0, padx=2, pady=1, sticky="ew")
addButton = tk.Button(self, text="Add", cursor="arrow")
addButton.grid(row=1, column=1, padx=10, pady=2, sticky="ew")
newLabel = tk.Label(self, text="Test", bg="#5522FF")
newLabel.grid(row=0, padx=2, pady=2, sticky="ew", columnspan=2)
newLabel.columnconfigure(0, weight=1)
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent)
self.parent=parent
self.grid()
self.columnconfigure(0, weight=1)
self.grid_rowconfigure(0,weight=1)
self.text = tk.Text(self, wrap="none", bg="#AA3333")
vsb = tk.Scrollbar(orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self.text.pack(fill="both", expand=True)
b = tk.Button(self, text="Button #%s" % 1, command=self.OnButtonClick)
self.text.window_create("end", window=b)
self.text.insert("end", "\n")
def OnButtonClick(self):
self.text.configure(state="normal")
panel = NewEntry(self.text, bg="#FF1111")
self.text.window_create("end", window=panel)
self.text.insert("end", "\n")
self.text.configure(state="disabled")
if __name__=="__main__":
root = tk.Tk()
root.resizable(True, True)
appinstance=MainApplication(root)
appinstance.pack(fill="both", expand=True)
root.mainloop()
I've read many different posts talking about grid_columnconfigure, fill options, sticky options, etc, but I haven't been able to get it filling properly. I am wondering if the window_create() method of the Text widget creates some sort of size limitation? It seems as though my code in NewEntry class is properly filling the space allowed by the window_create method, but I don't know how to create a "panel" to fill the width of the text box.
I am also aware of the possibility of using a canvas instead of text box (I'm wanting to maintain dynamic size and scrollability, though). I read from several different posts that a text widget is easiest if you have a simple stack of widgets, though. I will accept any recommendation, though.
The root of the problem is that a frame typically wants to fit its contents. So, when you add the label, entry widget, and button to the frame, it will shrink to fit. Any size you give to the frame will be ignored.
There are several solutions to this problem. What many people do is turn geometry propagation off (eg: self.grid_propagate(False)) but that means you have to manage both the width and height when all you really want is to control the width.
Another solution is to put something inside the panel that can be configured with an explicit width, and which will then cause the containing frame to grow to fit. For example, you can add an invisible frame in row 0 that sits behind the other widgets in row 0. When you change the width of this frame it will cause the containing frame to grow:
class NewEntry(tk.Frame):
def __init__(...):
...
# create this _before_ creating the other widgets
# so it is lowest in the stacking order
self.sizer = tk.Frame(self, height=1)
self.sizer.grid(row=0, columnspan=2)
...
With that, you can force the panel to be any width you want by setting the width of the sizer, but tkinter will continue to compute the optimum height for the widget:
self.sizer.configure(width=200)
Now you just need to set up bindings so that whenever the text widget changes size, you call this function to resize each entry.
For example, you might want to save all of the panels in a list so that you can iterate over them later.
class MainApplication(...):
def __init__(...):
...
self.panels = []
...
def OnButtonClick(...):
...
panel = NewEntry(...)
self.panels.append(panel)
...
With that, you can set up a binding that triggers whenever the window resizes:
class MainApplication(...):
def __init__(...):
...
self.text.bind("<Configure>", self.OnConfigure)
...
def OnConfigure(self, event):
for panel in self.panels:
panel.sizer.configure(width=event.width)
I wouldn't do it precisely like that since it tightly couples the implementation of the panel to the main window, but it illustrates the general technique of explicitly controlling the width of an embedded window.
Other solutions involve putting the panels inside a containing frame, and make that frame be the only widget added to the text widget. You could also use a canvas instead of a text widget since it allows you to explicitly set the width and height of embedded windows.