Create resizable Tkinter frame inside of scrollable canvas - python

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

Related

Tkinter grid vs pack manager window shrink

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.

Why isn't Listbox covering whole Frame in Tkinter?

I'm creating a simple GUI with Python Tkinter, and using Frame as containers. When I added a Listbox inside the Frame, it only occupies a small space in the Frame, rest space remains empty. I want it to cover whole Frame. Here's the code, and the output as image:
from tkinter import *
root = Tk()
root.geometry('1280x720')
root.title("Music Player")
root.grid_rowconfigure((0,1,2,3,4,5), weight=1)
root.grid_columnconfigure((0,), weight=1)
root.grid_columnconfigure((1,), weight=5)
root.resizable(False, False)
listframel = Frame(root, highlightbackground="black", highlightthickness=5)
listframel.grid(row=0, column=0, sticky=NSEW, rowspan=5)
listframer = Frame(root, highlightbackground="black", highlightthickness=5)
listframer.grid(row=0, column=1, sticky=NSEW, rowspan=5)
listbox = Listbox(listframel)
listbox.grid(row=0, column=0, rowspan=5, sticky=NSEW)
lst = list(x for x in range(100))
for i in range(len(lst)):
listbox.insert(i+1, lst[i]+1)
root.mainloop()
You need to apply grid_rowconfigure and grid_columnconfigure to all containers (root, listframel and listframer).

tkinter scrollbar alignment within LabelFrame

I want to display images on a Canvas within a Frame. For that I want the Canvas to have a Scrollbar. So I created a LabelFrame and assigned a Canvas for it:
wrapper1 = LabelFrame(self.root)
wrapper1.grid(row=1, columnspan=6, sticky="WE")
image_canvas = Canvas(wrapper1)
image_canvas.grid(sticky="NS")
Then I tried to add the Scrollbar:
yscrollbar = Scrollbar(wrapper1, orient="vertical", command=image_canvas.yview)
yscrollbar.grid(sticky="SNE", row=0, column=6)
image_canvas.configure(yscrollcommand=yscrollbar.set)
image_canvas.bind("<Configure>", lambda e: image_canvas.configure(scrollregion=image_canvas.bbox("all")))
All of it works apart from the fact that the Scrollbar doesn't properly stick to the right edge of the Frame:
As you can see it's rather in the middle. I also tried the same thing with buttons to no avail. Prior to this I also set up some buttons on self.root. I don't know if they mess up the grid somehow, as they are not in the LabelFrame:
BrowseButton = Button(self.root, text="Test1", command=self.browse)
BrowseButton.grid(row=3, column=0, sticky="WE")
ConvertButton = Button(self.root, text="Test2", command=self.search)
ConvertButton.grid(row=3, column=5, sticky="WE")
SimilarImages = Button(self.root, text="Test3", command=self.compare)
SimilarImages.grid(row=2, column=5, sticky="WE")
How do you make the Scrollbar stick correctly to the right side, while also filling out the whole Frame vertically?

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

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)

Categories

Resources