Tkinter grid vs pack manager window shrink - python

I have an example code that use the Tkinter grid manager for creating and allocating four squares:
root=tk.Tk()
root.rowconfigure(0,weight=1)
root.rowconfigure(1,weight=1)
root.columnconfigure(0,weight=1)
root.columnconfigure(1,weight=1)
canv1=tk.Canvas(root, bg="blue")
canv2 = tk.Canvas(root, bg="yellow")
canv3 = tk.Canvas(root, bg="red")
canv4 = tk.Canvas(root, bg="green")
canv1.grid(row=0, column=0, sticky="nsew")
canv2.grid(row=0, column=1, sticky="nsew")
canv3.grid(row=1, column=0, sticky="nsew")
canv4.grid(row=1, column=1, sticky="nsew")
root.mainloop()
After the main window is created, the squares are proportionally expanding and shrinking when the window size is changed by mouse dragging. Squares are always changed proportionally whenever the windows change their size by dragging and moving any of its edge or window corners.
I'm trying to get this same effect with pack manager. So I have the code:
root=tk.Tk()
upper=tk.Frame(root)
lower=tk.Frame(root)
canv1=tk.Canvas(upper,bg="blue")
canv2 = tk.Canvas(upper, bg="yellow")
canv3 = tk.Canvas(lower, bg="red")
canv4 = tk.Canvas(lower, bg="green")
canv1.pack(side='left', fill='both', expand=True)
canv2.pack(side='right', fill='both', expand=True)
canv3.pack(side='left', fill='both', expand=True)
canv4.pack(side='left', fill='both', expand=True)
upper.pack(side='top', fill='both', expand=True)
lower.pack(side='bottom', fill='both', expand=True)
root.mainloop()
When the pack manager is used the squares are only expanded proportionally, when the size of window is changed. During the shrinking (by dragging some edge or corner), the squares not change their size proportionally.
I would like to ask - is it possible to make the squares shrink proportionally while changing windows size using pack manager?

The packer tries to preserve the original size of the widgets as long as possible. If a widget isn't large enough to fit, it only shrinks the widget that won't fit in its preferred size. Thus, if you shrink the window horizontally, the widgets on the right will shrink so that the size of the widgets on the left are preserved.
I think the only workaround is to give every canvas a preferred size of 1x1. If you do that, and then give your window as a whole a geometry (so that the entire window isn't just a couple pixels in size) you will get the behavior you want.
root.geometry("800x800")
...
canv1 = tk.Canvas(upper,bg="blue", width=1, height=1)
canv2 = tk.Canvas(upper, bg="yellow", width=1, height=1)
canv3 = tk.Canvas(lower, bg="red", width=1, height=1)
canv4 = tk.Canvas(lower, bg="green", width=1, height=1)
For this specific problem, I see no advantage to using pack over grid, since you are in fact creating a grid of widgets.

Related

Create resizable Tkinter frame inside of scrollable canvas

There is an application I am working on using Python and Tkinter, and this application requires a variety of input forms. Each input form needs a scrollbar in case the form’s parent is too short to display everything, and so after some help from Google, this is what I currently have:
import tkinter as tk
def get_vertically_scrollable_frame(parent_frame: tk.Frame or tk.Tk) -> tk.Frame:
"""
:param parent_frame: The frame to place the canvas and scrollbar onto.
:return: A scrollable tk.Frame object nested within the parent_frame object.
"""
assert isinstance(parent_frame, tk.Frame) or isinstance(parent_frame, tk.Tk)
# Create the canvas and scrollbar.
canvas = tk.Canvas(master=parent_frame, bg="blue")
scrollbar = tk.Scrollbar(master=parent_frame, orient=tk.VERTICAL, command=canvas.yview)
# Let the canvas and scrollbar resize to fit the parent_frame object.
parent_frame.rowconfigure(0, weight=1)
parent_frame.columnconfigure(0, weight=1)
canvas.grid(row=0, column=0, sticky='news')
scrollbar.grid(row=0, column=1, sticky='nes')
# Link the canvas and scrollbar together.
canvas.configure(yscrollcommand=scrollbar.set)
canvas.bind('<Configure>', lambda x: canvas.configure(scrollregion=canvas.bbox("all")))
# Create the tk.Frame that is within the canvas.
canvas_frame = tk.Frame(master=canvas, bg="red")
canvas.create_window((0, 0), window=canvas_frame, anchor="nw")
# TODO: Let the canvas_frame object resize to fit the parent canvas.
canvas.columnconfigure(0, weight=1)
canvas.rowconfigure(0, weight=1)
# canvas_frame.grid(row=0, column=0, sticky="news") # Resizes the frame, but breaks the scrollbar.
return canvas_frame
if __name__ == "__main__":
window = tk.Tk()
window.rowconfigure(0, weight=1)
window.columnconfigure(0, weight=1)
parent_frame = tk.Frame(master=window)
parent_frame.grid(row=0, column=0, sticky="news")
scrollable_frame = get_vertically_scrollable_frame(parent_frame)
# Add the widgets to the new frame.
scrollable_frame.columnconfigure(0, weight=1) # Resize everything horizontally.
tk.Label(master=scrollable_frame, text="First name").grid(row=0, column=0, sticky="w")
tk.Entry(master=scrollable_frame).grid(row=1, column=0, sticky="ew")
tk.Label(master=scrollable_frame, text="").grid(row=2, column=0, sticky="w")
tk.Label(master=scrollable_frame, text="Last name").grid(row=3, column=0, sticky="w")
tk.Entry(master=scrollable_frame).grid(row=4, column=0, sticky="ew")
tk.Label(master=scrollable_frame, text="").grid(row=5, column=0, sticky="w")
tk.Label(master=scrollable_frame, text="Email").grid(row=6, column=0, sticky="w")
tk.Entry(master=scrollable_frame).grid(row=7, column=0, sticky="ew")
tk.Label(master=scrollable_frame, text="").grid(row=8, column=0, sticky="w")
tk.Label(master=scrollable_frame, text="Favorite color").grid(row=9, column=0, sticky="w")
tk.Entry(master=scrollable_frame).grid(row=10, column=0, sticky="ew")
tk.Label(master=scrollable_frame, text="").grid(row=11, column=0, sticky="w")
tk.Frame(master=scrollable_frame).grid(row=12, column=0, sticky="news")
scrollable_frame.rowconfigure(12, weight=1) # Vertically resize filler frame from last line.
tk.Button(master=scrollable_frame, text="Clear").grid(row=13, column=0, sticky="ews")
tk.Button(master=scrollable_frame, text="Submit").grid(row=14, column=0, sticky="ews")
window.mainloop()
This function takes an empty Tkinter frame, places a working canvas and scrollbar on that frame, places a new frame into the canvas, and then returns the frame inside the canvas.
While the scrollbar works fine with the above code, the returned nested Tkinter frame does not resize to fit the height and width of its parent canvas. If the parent canvas is too large, it looks like this:
(The blue area is the canvas, and red is the frame inside the canvas.)
In an attempt to fix this, I manually placed the nested frame on the canvas using grid (see the commented code just before the return statement). The frame inside the canvas started resizing itself, but the scrollbar stopped working.
Is there a simple way to allow the frame inside the canvas to resize itself without breaking the scrollbar?
Is there a simple way to allow the frame inside the canvas to resize itself without breaking the scrollbar?
Simple? I guess that depends on your definition of simplicity. It's possible, but it requires a few extra lines of code.
The scrollbars only work if you add the frame to the canvas with create_window and only when you let the frame be as big as it needs to be to hold all of its children and then set the canvas bbox accordingly. When the window resizes you need to force the frame to be bigger than it wants to be if it is smaller than the canvas, but you need to let the frame be its preferred size if the frame is bigger than the canvas.
The solution looks something like the following example, off the top of my head. Notice the use of a tag to make it easy to find the inner frame. You could just as easily store the id returned by create_window and use that instead. This also takes advantage of the fact that the event object has width and height attributes for the canvas.
def get_vertically_scrollable_frame(parent_frame: tk.Frame or tk.Tk) -> tk.Frame:
...
canvas.create_window((0, 0), window=canvas_frame, anchor="nw", tags=("canvas_frame",))
canvas.bind('<Configure>', handle_resize)
...
def handle_resize(event):
canvas = event.widget
canvas_frame = canvas.nametowidget(canvas.itemcget("canvas_frame", "window"))
min_width = canvas_frame.winfo_reqwidth()
min_height = canvas_frame.winfo_reqheight()
if min_width < event.width:
canvas.itemconfigure("canvas_frame", width=event.width)
if min_height < event.height:
canvas.itemconfigure("canvas_frame", height=event.height)
canvas.configure(scrollregion=canvas.bbox("all"))

How do I keep elements in a frame justified to the right while keeping the entire frame coloured, regardless of the size of the window?

I'm new to tkinter so I'm a little lost in terms of grid layout. What I'm trying to do is have a logo sit in the bottom right corner of the window, and always be in that position no matter how big the window is. I have managed to position the logo no problem, but when I justify to the right, the frame becomes white on the left side of the elements. How do I keep this part Black as it is under the logo? Left justify fills the whole frame with black, but right justify only fills from the logo/text onward.
This is what I am getting
Here is my current code:
from tkinter import *
root = Tk()
# GUI attributes
root.title('Lantern')
root.geometry('800x600')
root.iconbitmap('iconrl.ico')
# main containers
topFrame = Frame(root, bg='#000000', width=800, height=100, pady=3)
center = Frame(root, bg='#181818', padx=3, pady=3)
btmFrame = Frame(root, bg='#000000', width=800, height=90, padx=10)
# layout all of the main containers
root.grid_rowconfigure(1, weight=1)
root.grid_columnconfigure(0, weight=1)
topFrame.grid(row=0, sticky='ew')
center.grid(row=1, sticky='nsew')
btmFrame.grid(row=2, sticky='e')
# topFrame Widgets
rlLabel = Label(topFrame, text='Lantern ', font=('Verdana', 12), fg='red', bg='#000000', width=10)
# topFrame Layout
rlLabel.grid(row=0, columnspan=3)
# center Widgets
# center Layout
# btmFrame Widgets
powered = Label(btmFrame, text='Powered by: ', font=('Verdana', 12), fg='#FFF204', bg='#000000', width=15)
sLogo = PhotoImage(file='slogo.png')
sLogoLabel = Label(btmFrame, image=sLogo, bg='#000000')
# btmFrame Layout
powered.grid(row=0, column=1, sticky='e')
sLogoLabel.grid(row=0, column=2, sticky='e')
root.mainloop()
First you need to change btmFrame.grid(row=2, sticky='e') to btmFrame.grid(row=2, sticky='ew'), so that the frame fills all the space horizontally.
Then add btmFrame.columnconfigure(0, weight=1) to push the powered and sLogoLabel to the right of the frame.
Or you can use pack() on powered and sLogoLabel:
sLogoLabel.pack(side='right')
powered.pack(side='right')

How can I choose coordinate of tkinter frame?

I am experimenting with Tkinter, as I was trying to place tree frames. Two frames must be placed side by side: the red one and the blue one. (see frame image). I place this two frames with this istructions:
redFrame = tk.Frame(master=masterFrame, bg='red')
redFrame.pack_propagate(0)
redFrame.pack(fill='both', side='left', expand='True')
blueFrame = tk.Frame(master=masterFrame, bg='blue')
destFrame.pack_propagate(0)
destFrame.pack(fill='both', side='right', expand='True')
Now I want to put another frame (green) inside the red one, placed on the left side of the main interface:
greenFrame = tk.Frame(master=masterFrame, width=100, height=100, bg='green')
greenFrame.pack_propagate(0)
greenFrame.pack(side='bottom', padx=0, pady=0)
The problem is that I can't choose its position. It still remain in the top side of the window and centered.
If I change the padx parameter nothing change.
If i change the pady parameter, the frame change its Y position. Why this not appened with the X posistion?
Widgets will by default be centered in their allocated space within the container.
Your code at the moment doesn't work, so I've changed it to this (I think it mimics what you're saying you currently have):
import tkinter as tk
root = tk.Tk()
root.geometry("600x400")
redFrame = tk.Frame(root, bg='red')
redFrame.pack_propagate(0)
redFrame.pack(fill='both', side='left', expand='True')
blueFrame = tk.Frame(root, bg='blue')
blueFrame.pack_propagate(0)
blueFrame.pack(fill='both', side='right', expand='True')
greenFrame = tk.Frame(redFrame, width=100, height=100, bg='green')
greenFrame.pack_propagate(0)
greenFrame.pack(side='top', padx=0, pady=0)
root.mainloop()
If you run this code, the greenFrame will appear as your image shows, at the top and centered inside redFrame.
If you want greenFrame to appear at the top left of redFrame, you can change the anchor point of the frame:
greenFrame = tk.Frame(redFrame, width=100, height=100, bg='green')
greenFrame.pack_propagate(0)
greenFrame.pack(side='top', padx=0, pady=0, anchor='w')
That is because when you do side="top" using pack, the widget gets allocated an entire strip of horizontal space. It gets centered within that space by default:
If you do side="left", then the widget gets allocated an entire strip of vertical space, and it gets centered in that space by default.
However in both cases you can change the anchor point, so that your component is placed differently within that allocated spaces.
This is the code which achieves what (I think) you want:
import tkinter as tk
root = tk.Tk()
root.geometry("600x400")
redFrame = tk.Frame(root, bg='red')
redFrame.pack_propagate(0)
redFrame.pack(fill='both', side='left', expand='True')
blueFrame = tk.Frame(root, bg='blue')
blueFrame.pack_propagate(0)
blueFrame.pack(fill='both', side='right', expand='True')
greenFrame = tk.Frame(redFrame, width=100, height=100, bg='green')
greenFrame.pack_propagate(0)
greenFrame.pack(side='top', padx=0, pady=0, anchor='w')
root.mainloop()

Changing the size of a scrollbar in tkinter(using grid layout)

I'm making a Tkinter GUI that allows you to query a database and will display results in a scrollable frame. However, when you produce the results the scrollbar will not adjust to match the new size of the frame. How can I get the scrollbar to be able to display all of the results? I've put together a quick and dirty version of the code to demonstrate the problem I'm having.
import tkinter as tk
def Lookup():
list = frame_buttons.grid_slaves()
for l in list:
l.destroy()
for x in range(1000):
tk.Label(frame_buttons, text="test", background="white").grid(row=x)
root = tk.Tk()
root.grid_rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
frame_main = tk.Frame(root, bg="white")
frame_main.grid(row = 0,sticky='news')
frame_input = tk.Frame(frame_main, background = "white")
frame_input.grid(row=0, column=0, pady=(5, 0), sticky='nw')
tk.Button(frame_input, text="Search", fg="black", background = "grey",command=Lookup).grid(row=3, column=0,sticky='nw')
# Create a frame for the canvas with non-zero row&column weights
frame_canvas = tk.Frame(frame_main)
frame_canvas.grid(row=1, column=0, pady=(5, 0), sticky='nw')
frame_canvas.grid_rowconfigure(0, weight=1)
frame_canvas.grid_columnconfigure(0, weight=1)
# Set grid_propagate to False to allow 5-by-5 buttons resizing later
frame_canvas.grid_propagate(False)
# Add a canvas in that frame
canvas = tk.Canvas(frame_canvas, bg="gray")
canvas.grid(row=0, column=0, sticky="news")
# Link a scrollbar to the canvas
vsb = tk.Scrollbar(frame_canvas, orient="vertical", command=canvas.yview)
vsb.grid(row=0, column=1, sticky='ns')
canvas.configure(yscrollcommand=vsb.set)
frame_buttons = tk.Frame(canvas, bg="gray")
canvas.create_window((0, 0), window=frame_buttons, anchor='nw')
for x in range(15):
tk.Label(frame_buttons, text="blah", background = "white").grid(row=x)
frame_buttons.update_idletasks()
frame_canvas.config(width=500, height=100)
canvas.config(scrollregion=canvas.bbox("all"))
root.mainloop()
this initially puts 20 labels in the scroll region, which is enough to activate the scrollbar. Then when you click search it replaces those 20 lables with 1000 test labels. But only the first 20 will be viewable.
You need to reset the scrollregion whenever the frame changes size and the items are redrawn.
The normal way to do that is to bind to the <Configure> event of the frame so that it happens automatically as the frame grows and shrinks.
Example:
def reset_scrollregion(event):
canvas.config(scrollregion=canvas.bbox("all"))
...
frame_buttons.bind("<Configure>", reset_scrollregion)

tkinter frame propagate not behaving?

If you uncomment the options_frame_title you will see that it does not behave properly. Am I missing something? That section was just copied and pasted from the preview_frame_title and that seems to have no issues.
from tkinter import *
blank_app = Tk()
blank_app.geometry('750x500+250+100')
blank_app.resizable(width=False, height=False)
main_frame = Frame(blank_app, width=750, height=500, bg='gray22')
main_frame.grid(row=0, column=0, sticky=NSEW)
main_title = Label(main_frame, text='App Builder', bg='gray', fg='red', font='Times 12 bold', relief=RIDGE)
main_title.grid(row=0, column=0, padx=2, pady=2, sticky=NSEW, columnspan=2)
preview_frame = Frame(main_frame, width=70, height=465, bg='red', highlightcolor='white', highlightthickness=2)
preview_frame.grid(row=1, column=0, padx=2, pady=2, sticky=NSEW)
preview_frame_title = Label(preview_frame, text='Preview Window', width=70, bg='gray', fg='blue', relief=RIDGE)
preview_frame_title.grid(row=0, column=0, sticky=NSEW)
options_frame = Frame(main_frame, width=240, height=465, bg='blue', highlightcolor='white', highlightthickness=2)
options_frame.grid(row=1, column=1, padx=2, pady=2, sticky=NSEW)
options_frame_title = Label(options_frame, text='Widget Options', width=20, bg='gray', fg='blue', anchor=CENTER, relief=RIDGE)
options_frame_title.grid(row=0, column=0, sticky=NSEW)
blank_app.mainloop()
I don't understand what you mean by "behaving properly". It seems to be behaving as it's designed to behave.
By default, tkinter frames are designed to shrink (or grow) to fit their child widgets. When you comment out options_frame_title.grid(...), the frame has no visible children so it says the fixed size that you gave it. When you uncomment that line, it causes a label to be placed in the widget which then causes the frame to shrink to fit.
To further complicate the matters for you, grid will by default give any extra space to rows and columns that have a non-zero weight. Since you haven't given any rows or columns any weight, they don't get any extra space.
Part of the problem is that you are trying to solve too many problems at once. When first starting out you need to be more methodical. Also, you should consider using pack when you're putting a single widget into another widget. It only takes one line of code to get it to fill its parent rather than three with grid.
pro-tip: it really helps if you separate widget creation from widget layout. Your code, even though it's only a couple dozen lines long, is really hard to read.
For example, the first thing you should do is start by creating your top-most frames, and get them to fill and expand/shrink properly before putting any widgets in them.
Starting from scratch
Step 0: don't remove the ability to resize the window
User's don't like having control taken away from them. Remove this line:
blank_app.resizable(width=False, height=False)
Your users will thank you, and during development it's much easier to play with the window to make sure everything is filling, growing, and shrinking as necessary.
Step 1: main_frame
Since it appears this is designed to contain everything, it makes sense to use pack since it is the only widget directly in blank_app.
main_frame = Frame(blank_app, width=750, height=500, bg='gray22')
main_frame.pack(fill="both", expand=True)
With just that (plus the first couple of lines where you create the root window, along with the final call to mainloop), notice how the window is the right size, and the main frame fills the window. You can resize the window all you want and the main frame will continue to fill the whole window.
Step 2: widgets inside main_frame
As I mentioned earlier, it's best to separate widget creation and widget layout. Also, when using grid a good rule of thumb is to always give at least one row and one column a weight. It appears you want the right frame to be about 3x as wide as the left frame. This is where you can use weights.
# widgets in the main frame
main_title = Label(main_frame, text='App Builder', bg='gray', fg='red', font='Times 12 bold', relief=RIDGE)
preview_frame = Frame(main_frame, width=70, height=465, bg='red', highlightcolor='white', highlightthickness=2)
options_frame = Frame(main_frame, width=240, height=465, bg='blue', highlightcolor='white', highlightthickness=2)
# laying out the main frame
main_frame.grid_rowconfigure(1, weight=1)
main_frame.grid_columnconfigure(0, weight=1)
main_frame.grid_columnconfigure(1, weight=3)
main_title.grid(row=0, column=0, padx=2, pady=2, sticky="nsew", columnspan=2)
preview_frame.grid(row=1, column=0, padx=2, pady=2, sticky="nsew")
options_frame.grid(row=1, column=1, padx=2, pady=2, sticky="nsew")
Once again, run the code and notice that as you resize the main window everything still continues to fill the window, and resize properly, and keep the proper proportions. If you don't like the proportions, just change the weights. They can be any number you want. For example, you could use 70 and 240 if you want.
Step 3: preview frame
The preview frame has a label, and I presume you will be putting other stuff under the label. We'll continue to use grid, and just give the row below the label a weight so that it gets all of the extra space. When you add more widgets, you might need to adjust accordingly.
# widgets in the preview frame
preview_frame_title = Label(preview_frame, text="Preview Window", bg='gray', fg='blue', relief=RIDGE)
# laying out the preview frame
preview_frame.grid_rowconfigure(1, weight=1)
preview_frame.grid_columnconfigure(0, weight=1)
preview_frame_title.grid(row=0, column=0, sticky="nsew")
Step 4: the options frame
This is just like the preview frame: a label at the top, and all of the extra space is given to the empty row number 1.
# widgets in the options frame
options_frame_title = Label(options_frame, text='Widget Options', bg='gray', fg='blue', anchor=CENTER, relief=RIDGE)
# laying out the options frame
options_frame.grid_rowconfigure(1, weight=1)
options_frame.grid_columnconfigure(0, weight=1)
options_frame_title.grid(row=0, column=0, sticky="new")
Final thoughs
Notice that you don't need to worry about propagation, which is somewhat of an advanced topic. You also don't have to worry about the size of frames since we're using column weights to give relative sizes, and you don't have to give sizes to their labels.
We have removed the propagation code, removed the non-resizable behavior, and removed some hard-coded widths, giving us less code but more functionality.
Ok after some digging I realized the problem was not with your options_frame_title but with your frames the Label was being placed in.
Of you un-comment options_frame_title and comment out preview_frame_title you will see the exact same problem. What is happening is the frame has a set size and the main window is conforming to that frame size. And when you decide to place a label into the frame then the frame will conform to the label size.
What you want to do to achieve the look you are going for is do something a little different with the .grid_propagate(0) than what you are currently doing.
We also need to add some weights to the correct frames so the widgets will fill properly.
Take a look at this code.
from tkinter import *
blank_app = Tk()
main_frame = Frame(blank_app,width=700, height=300, bg='gray22')
main_frame.grid(row=0, column=0, sticky=NSEW)
main_frame.grid_propagate(0) #the only place you need to use propagate(0) Thought there are better ways
main_frame.columnconfigure(0, weight = 1) #using weights to manage frames properly helps a lot here
main_frame.columnconfigure(1, weight = 1)
main_frame.rowconfigure(0, weight = 0)
main_frame.rowconfigure(1, weight = 1)
main_title = Label(main_frame, text='App Builder', bg='gray', fg='red', font='Times 12 bold', relief=RIDGE)
main_title.grid(row=0, column=0, columnspan=2, padx=2, pady=2, sticky=NSEW)
preview_frame = Frame(main_frame, bg='red', highlightcolor='white', highlightthickness=2)
preview_frame.grid(row=1, column=0, padx=2, pady=2, sticky=NSEW)
preview_frame.columnconfigure(0, weight = 1)# using weights to manage frames properly helps a lot here
preview_frame_title = Label(preview_frame, text='Preview Window', bg='gray', fg='blue', relief=RIDGE)
preview_frame_title.grid(row=0, column=0, sticky=NSEW)
options_frame = Frame(main_frame, bg='blue', highlightcolor='white', highlightthickness=2)
options_frame.grid(row=1, column=1, padx=2, pady=2, sticky=NSEW)
options_frame.columnconfigure(0, weight = 1) #using weights to manage frames properly helps a lot here
options_frame_title = Label(options_frame, text='Widget Options', bg='gray', fg='blue', anchor=CENTER, relief=RIDGE)
options_frame_title.grid(row=0, column=0, sticky=NSEW)
blank_app.mainloop()

Categories

Resources