tkinter - How to get frames and widgets to fill window? - python

The two frames in my program, ViewSubjects and AddSubjects both contain frames and widgets that are not stretching to fill the screen. Why are they doing this and how would I go about fixing it?
ViewSubjects Frame
AddSubjects Frame
Here is my code:
import tkinter as tk
from tkinter import ttk
import tkinter.scrolledtext as tks
class Program(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.iconbitmap(self, default = "")
tk.Tk.wm_title(self, "")
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 (SubjectHome, ViewSubject, AddSubject):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row = 0, column = 0, sticky = "nsew")
self.show_frame(SubjectHome)
def show_frame(self,cont):
frame = self.frames[cont]
frame.tkraise()
class SubjectHome(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
ttk.Style().configure("TButton", padding=6, relief="flat", background="#ccc")
name = tk.Label(self, text = "User: FirstName + LastName")
name.pack(anchor="ne")
pagename = tk.Label(self, text = "Subject Menu")
pagename.pack(anchor="n")
self.innerFrame = tk.Frame(self, bg="red")
self.innerFrame.place(relx=.5, rely=.5, anchor="c")
view = ttk.Button(self.innerFrame, text = "View Subjects", command = lambda: controller.show_frame(ViewSubject))
view.grid(row=0, sticky="W"+"E")
add = ttk.Button(self.innerFrame, text = "Add Subjects", command = lambda: controller.show_frame(AddSubject))
add.grid(row=1, sticky="W"+"E")
class ViewSubject(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
innerFrame = tk.Frame(self)
innerFrame.place(relx=.5, rely=.5, anchor="c")
firstFrame = tk.Frame(innerFrame)
firstFrame.grid(row=0, sticky="WE")
secondFrame = tk.Frame(innerFrame)
secondFrame.grid(row=1, sticky="WE")
self.text = tks.ScrolledText(firstFrame)
self.text.grid(rowspan=3, columnspan=3 ,sticky="E")
scrollbar = tk.Scrollbar(secondFrame, orient="vertical")
lb = tk.Listbox(secondFrame, yscrollcommand=scrollbar.set)
scrollbar.config(command=lb.yview)
scrollbar.pack(side="right", fill="y")
lb.pack(side="left",fill="both", expand=True)
for x in range(15):
lb.insert("end", x)
back = ttk.Button(innerFrame, text = "Back", command = lambda: controller.show_frame(SubjectHome))
back.grid(row=2, sticky="W")
next = ttk.Button(innerFrame, text = "Next")
next.grid(row=2, sticky="E")
class AddSubject(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
name = tk.Label(self, text = "User: FirstName + LastName")
name.pack(anchor="ne")
pagename = tk.Label(self, text = "Add Subjects")
pagename.pack(anchor="n")
self.innerFrame = tk.Frame(self)
self.innerFrame.place(relx=.5, rely=.5, anchor="c")
canvas = tk.Canvas(self.innerFrame)
self.firstFrame = tk.Frame(canvas)
self.firstFrame.pack(anchor="n")
info = tk.Label(self.innerFrame, text = "Information...\n Information....")
info.pack()
for x in range(5):
pagename = tk.Label(self.firstFrame, text = "Unit Name")
pagename.grid(row=0, column=x)
self.text = tks.ScrolledText(self.firstFrame, width=50)
self.text.grid(row=1, column=x ,sticky="E")
scrollbar = tk.Scrollbar(self.innerFrame, orient="horizontal", command=canvas.xview)
canvas.configure(xscrollcommand=scrollbar.set)
scrollbar.pack(side="bottom", fill="x")
canvas.pack(side="left", fill="both", expand=True)
back = ttk.Button(self.innerFrame, text = "Back", command = lambda: controller.show_frame(SubjectHome))
back.pack(anchor="sw")
next = ttk.Button(self.innerFrame, text = "Next")
next.pack(anchor="se")
app = Program()
app.state('zoomed')
app.mainloop()

Most of your problems boil down to three common mistakes:
First, you are using place, and place by default doesn't make windows grow or shrink. It assumes you created widgets to be the exact size you want them to be. You need to explicitly tell tkinter how you want it to handle extra space.
In your case you probably want to set the relative width and relative height of the inner frames to 1.0 (ie: 100% of the width and height of the containing window) with the relwidth and relheight options. This will force the inner frame to always be exactly as tall and wide as its parent.
self.innerFrame.place(..., relwidth=1.0, relheight=1.0)
Second, you are using grid within the inner frames, but you are failing to give any rows or columns a weight. The weight tells tkinter how to allocate extra space. By default, extra space goes unused. Since you want your widgets to fill the extra space, you need to give at least one row and one column a positive weight.
Third, you're trying to solve too many problems at once. My advice is to remove all of the widgets except one frame. Get it to fill, grow and shrink the way you want. Then, add its immediate children and do likewise. Then, add any grandchildren, and continue on, one group of widgets at a time, until you're satisfied with that frame. Only then should you move on and fix another frame.
protip: give each frame a unique color during development. This makes it easy to see which frames are growing or shrinking, and which ones are not.

Related

Menu item in Tkinter is not working properly (in MacOS)

I am making a tkinter app with multiple pages. The simple version of the codes I used is shown below. I added the menu to the root of the app and gave one option of File with sub-option of "Reload Defaults" and "Exit".
When I run this code then I do get the menu but not on the GUI but the main menu bar of Mac. And the menu is unresponsive. I am not sure what I am doing wrong!
import tkinter as tk
from tkinter import ttk
from tkinter import RAISED
LARGEFONT =("Verdana", 35)
class tkinterApp(tk.Tk):
# __init__ function for class tkinterApp
def __init__(self, *args, **kwargs):
# __init__ function for class Tk
tk.Tk.__init__(self, *args, **kwargs)
# creating a container
container = tk.Frame(self)
container.pack(side = "top", fill = "both", expand = True)
container.grid_rowconfigure(0, weight = 1)
container.grid_columnconfigure(0, weight = 1)
container2 = tk.Frame(self, relief=RAISED, borderwidth=2)
container2.pack(side="bottom",fill="both", expand=True)
container2.grid_rowconfigure(0, weight = 1)
container2.grid_columnconfigure(0, weight = 1)
def runProg():
print("response from runProg")
#get all entry value from page 2
runButton = ttk.Button(container2, text="Run", command=runProg)
runButton.pack(side="left", padx=5, pady=5)
######################################################
## Menu items
progMenu = tk.Menu(self,tearoff=0)
self.config(menu=progMenu)
#create File menu
def reload_command():
pass
fileMenu = tk.Menu(progMenu)
progMenu.add_cascade(label="File", menu=fileMenu)
fileMenu.add_command(label="Reload Defaults", command=reload_command)
fileMenu.add_command(label="Exit", command=reload_command)
######################################################
# initializing frames to an empty array
self.frames = {}
frame = StartPage(container, self)
self.frames[StartPage] = frame
frame.grid(row = 0, column = 0, sticky ="nsew")
self.show_frame(StartPage)
# to display the current frame passed as
# parameter
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
# first window frame startpage
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
# label of frame Layout 2
label = ttk.Label(self, text ="Startpage", font = LARGEFONT)
# putting the grid in its place by using
# grid
label.grid(row = 0, column = 2, padx = 10, pady = 10)
app = tkinterApp()
app.mainloop()
Any help would be greatly appreciated!
You might want to change the menu to be buttons at the top of the grid as on a Mac you will get this bug, but on windows it works perfectly fine.

Unable to get tkinter buttons to position correctly

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.

Python Tkinter, problem with creating unique ListBox on new pages

In tkinter I have step up multiple pages, each page should have a unique Listbox that will be populated with unique information.
My problem is the 'listbox' shows the information from my initial page on the other pages. Even if I completely remove the List box from the other pages, the 'Listbox` from my first page still shows up.
This is basically the first time I have used classes with anything, so I am not sure why this isn't working. This is all basically copied form YouTube how to's, and I am trying to bend it to what I need.
class Uploader(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs) #sets up tk window stuff
container = tk.Frame(self)
container.grid(row = 0, column = 0)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (Gr7, Gr8, Gr9): #put new pages on this
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column = 0, sticky = "nsew")
self.show_frame(Gr7)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class Gr7(tk.Frame):
def __init__(self,parent,controller):
tk.Frame.__init__(self, parent)
lb = tk.Listbox(width=30, height=15)
lb.insert('end', *homelist)
lb.grid(row = 1, column = 0)
class Gr8(tk.Frame):
def __init__(self,parent,controller):
tk.Frame.__init__(self, parent)
lb1 = tk.Listbox(width=30, height=15)
lb1.insert('end', *homelist1)
lb1.grid(row = 1, column = 0)
class Gr9(tk.Frame):
def __init__(self,parent,controller):
tk.Frame.__init__(self, parent)
lb2 = tk.Listbox(width=30, height=15)
lb2.insert('end', *homelist2)
lb2.grid(row = 1, column = 0)
You are not specifying which widget should contain the listbox, so all of your listboxes are given the root window as its master. Because you are putting them all in the same row and column, you only see one listbox.
To fix this -- and as a good general rule of thumb -- you should always explicitly provide the master when creating widgets:
lb = tk.Listbox(self, width=30, height=15)

How to create and display multiple widgets using a loop (Tkinter)

I am creating a program with two frames in one window. The first has input fields, the second will create a graph.
I found a way to create input fields dynamically from a list and get their values accordingly, but I can't get them to show on the window. When I run the program it shows an empty window.
What should I do to get the label and input widgets to show on the first frame (InputPage)? I tried changing 'parent' to 'self' but it made no difference. I don't really understand the structure of widgets in multiple frame applications.
Here is my code:
from tkinter import *
namesInput = ["first", "second", "third", "fourth", "fifth"]
entryInput = {}
labelInput = {}
root = Tk()
class ZorgplanGrafiek(Tk):
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
container = 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 (InputPage, GraphPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(InputPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class InputPage(Frame):
def __init__(self, parent, controller):
Frame.__init__(self,parent)
label = Label(self, text="Zorgplan input")
label.pack(pady=10,padx=10)
i = 0
for name in namesInput:
e = Entry(self)
entryInput[name] = e
lb = Label(self, text=name)
labelInput[name] = lb
i += 1
#def print_all_entries():
# for name in namesInput:
# print( entryInput[name].get())
class GraphPage(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
label = Label(self, text="The graph will show here")
label.pack(pady=10,padx=10)
button = Button(self, text="Back to Home",
command=lambda: controller.show_frame(InputPage))
button.pack()
app = ZorgplanGrafiek()
app.mainloop()
Firstly delete root = Tk() at the top of your code you are creating 2 windows. Secondly, your loop to create the entry and label widgets is not correct therefore they are not displayed on the frame, so that is your answer for why they wont show.
Try this:
class InputPage(Frame):
def __init__(self, parent, controller):
Frame.__init__(self,parent)
label = Label(self, text="Zorgplan input")
label.grid(row=0, column=0, sticky ='n', columnspan =2)
# i brought your variable in the class for example sake
namesInput = ["First:", "second:", "Third:", "Fourth:", "Fifth:"]
self.entryWidgets = [] # we want to call this in another function so we assign it as self.variableName
labelWidgets = []
#LOOP TO CREATE WIDGETS
for i in range(0, len(namesInput)):
labelWidgets.append(Label(self, text = namesInput[i]))
self.entryWidgets.append(Entry(self))
labelWidgets[-1].grid(row= i+1, column =0, sticky='e')
self.entryWidgets[-1].grid(row= i+1, column = 1, sticky='w')
submit = Button(self, text = "Submit", command = self.getEntries)
submit.grid(row = 6, column =0, columnspan =2)
def getEntries(self):
results = []
for x in self.entryWidgets: # i.e for each widget in entryWidget list
results.append(x.get())
print(results)
Code explanation:
We are iteratively creating widgets to the number of elements within namesInput list. Each time we create a widget we add it to their respective list. E.g for entry widgets we created a list called entryWidgets. We append them to a list so that we can reference them individually later on when we want to do something with them.
Furthermore, i changed pack() to grid(). The grid method is much cleaner and gives us more control over the layout of our window in my opinion.
Note - If you're struggling to understand how i 'grided' the widgets in the way i did, i just drew up a quick sketch of the widgets with co-ordinates representing their row and column and then from there its fairly easy to see how to manipulate the grid settings in the for loop.
Screenshot:

tkinter - Going Back and Forth Between Frames Using Buttons

I need functions, preferably one function, that can go back and forth between pages when the next and back buttons are pressed. I imagine this could be done by assigning boolean variables to the back and next buttons (not sure if this can be done) to figure out if you're going foward or back down an ordered list of all the pages. The index of the currently raised frame will need to be known. The indexes could be used to figure out the next page and then it would be raised. If the current index is 0 or the last index (in this case 2) and you press back or next respectively, then you would go to a homepage class frame, in this case BlankPage.
import tkinter as tk
from tkinter import ttk
class Program(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.iconbitmap(self, default = "")
tk.Tk.wm_title(self, "")
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 (Add, BlankPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row = 0, column = 0, sticky = "nsew")
self.show_frame(Add)
def show_frame(self,cont):
frame = self.frames[cont]
frame.tkraise()
class Add(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
innerFrame = tk.Frame(self)
innerFrame.place(relx=.5, rely=.5, anchor="c", relwidth=1.0, relheight=1.0)
innerFrame.grid_rowconfigure(1, weight=1)
innerFrame.grid_columnconfigure(0, weight=1)
name = tk.Label(innerFrame, text = "User")
name.grid(row=0, sticky="NE")
pagename = tk.Label(innerFrame, text = "Label")
pagename.grid(row=0, sticky="N")
next = ttk.Button(innerFrame, text = "Next", command = self.changePage)
next.grid(row=2, sticky="E")
back = ttk.Button(innerFrame, text = "Back", command = self.changePage)
back.grid(row=2, sticky="W")
###########################################################################################################
self.pageThree = tk.Frame(innerFrame)
self.pageThree.grid(row=1)
self.pageThree.grid_rowconfigure(0, weight=1)
self.pageThree.grid_columnconfigure(0, weight=1)
pagename = tk.Label(self.pageThree, text = "Page 3")
pagename.grid(row=0, sticky="N")
###########################################################################################################
self.pageTwo = tk.Frame(innerFrame)
self.pageTwo.grid(row=1)
self.pageTwo.grid_rowconfigure(0, weight=1)
self.pageTwo.grid_columnconfigure(0, weight=1)
pagename = tk.Label(self.pageTwo, text = "Page 2")
pagename.grid(row=0, sticky="N")
###########################################################################################################
self.pageOne = tk.Frame(innerFrame)
self.pageOne.grid(row=1)
self.pageOne.grid_rowconfigure(0, weight=1)
self.pageOne.grid_columnconfigure(0, weight=1)
pagename = tk.Label(self.pageOne, text = "Page 1")
pagename.grid(row=0, sticky="N")
###########################################################################################################
def changePage(self,buttonBool):
pages = [self.pageOne,self.pageTwo,self.pageThree]
#find current raised page and set to variable 'current'
position = pages.index(current)
if (postion==0 and buttonBool==False) or (postion==len(pages)-1 and buttonBool==True):
show_frame(BlankPage)
elif buttonBool==True:
pages[position+1].tkraise()
else:
pages[position-1].tkraise()
class BlankPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
app = Program()
app.state('zoomed')
app.mainloop()
The changePage function is my attempt at this, how would I complete it?
You are very close to having it all working, after some looking myself I couldn't find any (not overly-complicated) way to figure out the top most Frame so it would probably be best to just keep a record of the current position:
def __init__(self, parent, controller):
...
self.position = 0 #the index of the pages list
And to get buttonBool to be passed to changePage you can something from here (Tlapička gives the best solution in my eyes since lambda expressions make the lines of code way too long)
def __init__(self, parent, controller):
...
# button commands don't have an event but sometimes you use these callbacks for both .bind and buttons
# so having event=None makes it work for both.
def go_next(event=None):
self.changePage(True)
next = ttk.Button(innerFrame, text = "Next", command = go_next)
next.grid(row=2, sticky="E")
def go_back(event=None):
self.changePage(False)
back = ttk.Button(innerFrame, text = "Back", command = go_back)
back.grid(row=2, sticky="W")
...
With these two (and implementing self.position into changePage) you can accomplish what you originally asked, everything below this is the code reviewer in me talking.
Although using a boolean would work, this strategy of dealing with extra arguments to callbacks lets you pass any argument into changePage so it would probably simplify the conditionals in changePage if it got the change in pages (so 1 or -1):
def go_next(event=None):
self.changePage(1)
next = ttk.Button(innerFrame, text = "Next", command = go_next)
next.grid(row=2, sticky="E")
def go_back(event=None):
self.changePage(-1)
back = ttk.Button(innerFrame, text = "Back", command = go_back)
back.grid(row=2, sticky="W")
#this is for the last suggestion
self.nextButton = next
self.backButton = back
...
then changePage could look like this although I'm not sure what would happen to self.position if you changed to an invalid page:
def changePage(self,change):
pages = [self.pageOne,self.pageTwo,self.pageThree]
new_position = self.position + change
if (new_postion < 0) or (new_postion <= len(pages)):
show_frame(BlankPage)
#not sure how you would handle the new position here
else:
pages[new_position].tkraise()
self.position = new_position
Even better, if you keep a reference to the next and back buttons you can config them to indicate that it is the end/beginning:
def changePage(self,change):
pages = [self.pageOne,self.pageTwo,self.pageThree]
new_position = self.position + change
if (0 <= new_postion < len(pages)):
pages[new_position].tkraise()
self.position = new_position
else:
show_frame(BlankPage)
if new_position+1 >= len(pages):
self.nextButton.config(text="End") #, state=tk.DISABLED)
else:
self.nextButton.config(text="Next") #, state=tk.NORMAL)
if new_position-1 < 0:
self.backButton.config(text="First") #, state=tk.DISABLED)
else:
self.backButton.config(text="Back") #, state=tk.NORMAL)
that way you would know when you reached the end even if there isn't indication from the contents. (or you could disable the buttons to prevent going past)

Categories