Scrollbar widgets won't work when laid out in a grid - python

When using Python3 with the tkinter library, it's happened to me several times that I've needed a scrollable frame (with both vertical and horizontal scrolling capability) in order to contain a related set of widgets. Since scrollbars aren't easily attached to frames (I hear you can attach scrollbars to canvases, but not frames), I decided to create my own ScrolledFrame class -- one that uses scrollbars with a canvas widget that displays a frame inside it.
For the most part, it works pretty well. I laid-out the scrollbars and the canvas with the .pack() method. But because they are pack()ed, the vertical widget goes down all the way to the bottom of the screen, instead of leaving a blank space. (Run my code to see what I mean.)
#!/bin/env python3
# File: scrolledframe.py
# Written by: J-L
import tkinter as tk
class ScrolledFrame(tk.Frame):
def __init__(self, parent, **kwargs):
# Create the "scaffolding" widgets:
self.outerFrame = outerFrame = tk.Frame(parent, **kwargs)
self.canvas = canvas = tk.Canvas(outerFrame)
self.vscroll = vscroll = tk.Scrollbar(outerFrame, orient='vertical')
self.hscroll = hscroll = tk.Scrollbar(outerFrame, orient='horizontal')
# Pack/grid the vscroll, hscroll, and canvas widgets into their frame:
usePack = True
if usePack: # use .pack()
vscroll.pack(side='right', fill='y')
hscroll.pack(side='bottom', fill='x')
canvas.pack(fill='both', expand=True)
else: # use .grid()
canvas.grid(row=0, column=0, sticky='nsew')
vscroll.grid(row=0, column=1, sticky='ns')
hscroll.grid(row=1, column=0, sticky='ew')
# Hook up the commands for vscroll, hscroll, and canvas widgets:
vscroll.configure(command=canvas.yview)
hscroll.configure(command=canvas.xview)
canvas.configure(yscrollcommand=vscroll.set,
xscrollcommand=hscroll.set)
# Now create this very obejct (the innerFrame) as part of the canvas:
super().__init__(canvas)
innerFrame = self
canvas.create_window((0, 0), window=innerFrame)
innerFrame.bind('<Configure>',
lambda event: canvas.configure(scrollregion=canvas.bbox('all'),
width=event.width,
height=event.height))
# Accessor methods to access the four widgets involved:
def outer_frame(self): return self.outerFrame
def vscroll(self): return self.vscroll
def hscroll(self): return self.hscroll
def inner_frame(self): return self # (included for completeness)
# When .pack(), .grid(), or .place() is called on this object,
# it should be invoked on the outerFrame, attaching that to its
# parent widget:
def pack(self, **kwargs): self.outerFrame.pack(**kwargs)
def grid(self, **kwargs): self.outerFrame.grid(**kwargs)
def place(self, **kwargs): self.outerFrame.place(**kwargs)
def doExample():
# Create the main window:
root = tk.Tk()
root.title('ScrolledFrame Example')
# Create the scrolledFrame and a quit button:
scrolledFrame = ScrolledFrame(root)
scrolledFrame.pack()
tk.Button(root, text='Quit', command=root.quit).pack(side='bottom')
# Create some labels to display inside the scrolledFrame:
for i in range(1, 30+1):
tk.Label(scrolledFrame,
text='This is the text inside label #{}.'.format(i)
).grid(row=i-1, column=0)
# Start the GUI:
root.mainloop()
if __name__ == '__main__':
doExample()
To be honest, this isn't a big problem, but I then I thought about using the .grid() layout approach, by putting each scrollbar in its own row and own column.
Around line 18 of my code, I've included this line:
usePack = True
If you change it from True to False, the scrollbars and canvas widgets will be laid-out using .grid() instead of .pack(), and then you'll be able to see what I'm talking about.
So when I use .grid() to layout the scrollbars, the space under the vertical scrollbar does indeed appear as I'd expect it to, but now none of the scrollbars work!
This seems strange to me, as I don't understand why simply changing the layout managing of the widgets should make them behave any differently.
Question 1: What am I doing wrong that prevents the scrollbars from working when they are laid-out with .grid()?
Also, I notice that, with both .pack() and .grid(), the "Quit" button will move out-of-window as soon as I resize the window to be shorter than what it started out with.
Question 2: Is there a way I can force the "Quit" button to stay on the window (when the window is resizing), at the expense of my ScrolledFrame?
Thanks in advance for all your help!

What am I doing wrong that prevents the scrollbars from working when they are laid-out with .grid()?
The problem is that you aren't telling grid what to do with extra space, and what to do when there isn't enough space. Because of that, the widgets take up exactly the amount of space that they need.
With pack you're telling it to fill all allocated space with the fill and expand options. With grid, you need to give non-zero weight to the row and column that the canvas is in.
Add these two lines and you'll get the same behavior with grid that you do with pack:
outerFrame.grid_rowconfigure(0, weight=1)
outerFrame.grid_columnconfigure(0, weight=1)
You may also want to use the fill and expand option when packing scrolledFrame, assuming you want it to completely fill the window.
scrolledFrame.pack(fill="both", expand=True)
Is there a way I can force the "Quit" button to stay on the window (when the window is resizing), at the expense of my ScrolledFrame?
Call pack on it before you call pack the other widgets. When there isn't enough room for all the widgets and tkinter simply must reduce the size of one or more widgets to get it to fit, it starts by reducing the size of the last widget that was added.

Related

Python tkinter window resizes, but widgets never change or move

Python beginner. I placed a scrollbar widget in window and that works, but no matter what I do I can't get the scrollbox widget to change size. Could go with a larger scrollbox or for it to resize when the window resizes, but can't figure out how to force either to happen. Tried lots of different solutions, but feels like the grid and canvas are defaulting to a size and can't figure out how to change that. Help would be appreciated. Code is below:
import tkinter as tk
from tkinter import ttk
import os
import subprocess
class Scrollable(tk.Frame):
"""
Make a frame scrollable with scrollbar on the right.
After adding or removing widgets to the scrollable frame,
call the update() method to refresh the scrollable area.
"""
def __init__(self, frame, width=16):
scrollbar = tk.Scrollbar(frame, width=width)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=True)
self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.canvas.yview)
self.canvas.bind('<Configure>', self.__fill_canvas)
# base class initialization
tk.Frame.__init__(self, frame)
# assign this obj (the inner frame) to the windows item of the canvas
self.windows_item = self.canvas.create_window(0,0, window=self, anchor=tk.NW)
def __fill_canvas(self, event):
"Enlarge the windows item to the canvas width"
canvas_width = event.width
self.canvas.itemconfig(self.windows_item, width = canvas_width)
def update(self):
"Update the canvas and the scrollregion"
self.update_idletasks()
self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))
root = tk.Tk()
root.title("application")
root.geometry('750x800')
dbEnvs = ['a','b']
x = 1
header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack(side = "top")
body.pack()
footer.pack(side = "top")
#setup Environment selection
envLabel = tk.Label(header, text="Environment:")
envLabel.grid(row=0,column=0,sticky='nw')
dbselection = tk.StringVar()
scrollable_body = Scrollable(body, width=20)
x = 1
for row in range(50):
checkboxVar = tk.IntVar()
checkbox = ttk.Checkbutton(scrollable_body, text=row, variable=checkboxVar)
checkbox.var = checkboxVar # SAVE VARIABLE
checkbox.grid(row=x, column=1, sticky='w')
x += 1
scrollable_body.update()
#setup buttons on the bottom
pullBtn = tk.Button(footer, text='Pull')
pullBtn.grid(row=x, column=2, sticky='ew')
buildBtn = tk.Button(footer, text='Build')
buildBtn.grid(row=x, column=3, sticky='ew')
compBtn = tk.Button(footer, text='Compare')
compBtn.grid(row=x, column=4, sticky='ew')
root.mainloop()
have tried anchoring, changing the window base size and multiple other things (8 or 19 different items, plus reading lots of posts), but they normally involve packing and since I used grids that normally and ends with more frustration and nothing changed.
If you want the whole scrollbox to expand to fill the body frame, you must instruct pack to do that using the expand and fill options:
body.pack(side="top", fill="both", expand=True)
Another problem is that you're setting expand to True for the scrollbar. That's probably not something you want to do since it means the skinny scrollbar will be allocated more space than is needed. So, remove that attribute or set it to False.
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=False)
tip: when debugging layout problems, the problems are easier to visualize when you temporarily give each widget a unique color. For example, set the canvas to one color, body to another, the instance of Scrollable to another, etc. This will let you see which parts are visible, which are growing or shrinking, which are inside the borders of others, etc.

Tkinter: How to resize a scroll bar if the canvas changes size?

I am working on a project where I need to display information when a button is clicked, and hide information when that button is clicked again. The information can be several rows and extend beyond the window, so I am trying to add a scroll bar to get around this issue. The problem is that when the information is displayed, the scroll bar does not show. Scrolling is still possible but the actual bar is not there. Resizing the window fixes this issue for some reason. Also when the button is clicked again to hide the information, the size of the canvas and the scrollbar remain the same, so you can scroll far beyond the button into empty space. My theory is that the scroll region is not updating when the widgets in the canvas change size - but I'm not sure how to go about fixing that.
I realize this explanation may be a bit confusing, so I have provided a simplified example below. This example has a single button that when clicked reveals several lines of "other info". The scrollbar and/or canvas does not resize properly upon interacting with this button.
My strategy right now was to have a method called add_scrollbar that removes the current scrollbar and creates a new one every time the widgets change in hopes that the new one would be the right size; however this still is not working.
from tkinter import *
from tkinter import ttk
def add_scrollbar(outer_frame, canvas):
if len(outer_frame.winfo_children()) == 2:
# canvas is at index 0 of the outer frame, if a scrollbar has been added it will be at index 1
outer_frame.winfo_children()[1].destroy()
# my strategy here was to destroy the existing scroll bar
# and create a new one each time the widget changes size
scrollbar = ttk.Scrollbar(outer_frame, orient=VERTICAL, command=canvas.yview)
scrollbar.pack(side=RIGHT, fill=Y)
canvas.configure(yscrollcommand=scrollbar.set)
canvas.bind('<Configure>', lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
return
def create_example():
root = Tk()
root.geometry("1200x1200")
my_outer_frame = Frame(root)
my_outer_frame.pack(fill=BOTH, expand=1)
my_canvas = Canvas(my_outer_frame)
my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
inner_frame = Frame(my_canvas)
my_canvas.create_window((0, 0), window=inner_frame)
# ^^^ Sets up the ability to have a scroll_bar
changing_frame = Frame(inner_frame, borderwidth=4) # this is the frame that will be changing its contents
changing_frame.pack(side=LEFT, anchor="n")
display_frame(changing_frame, my_outer_frame, my_canvas)
# this method re-displays the changing frame depending on the specified size ('big' or 'small'
root.mainloop()
return
def display_frame(frame, outer_frame, canvas, size='small'):
for widget in frame.winfo_children():
widget.destroy()
if size == 'small':
Button(frame, height=5, width=5, text="Show",
command=lambda this_frame=frame: display_frame(this_frame, outer_frame, canvas, size='big')).grid(row=0,
column=0)
elif size == 'big':
Button(frame, height=5, width=5, text="Hide",
command=lambda this_frame=frame: display_frame(this_frame, outer_frame, canvas, size='small')).grid(
row=0, column=0)
for n in range(1, 100):
Label(frame, text="Other Stuff!").grid(row=n, column=0)
frame.pack(side=LEFT)
add_scrollbar(outer_frame, canvas) # this method is supposed to destroy the existing scrollbar and make a new one
return
if __name__ == '__main__':
create_example()

My frame is resizing instead of the listbox - tkinter

I'm making a GUI and I'm stuck trying to resize a listbox. The listbox is supposed to expand to fill the frame but instead, the frame shrinks to fit the listbox.
Thanks, in advance, for any help.
I have tried many variations of the code, but none seem to work, so I simplified the code (it still does't work) to put it on here.
import tkinter as tk
w = tk.Tk() # New window
f = tk.Frame(w, width=300, height=500, bg='red') # New frame with specific size
f.grid_propagate(0)
f.grid(row=0, column=0)
lb = tk.Listbox(f, bg='blue') # New listbox
lb.pack(fill=tk.BOTH, expand=True)
I ran these sequentially in IDLE and the frame appears (in red) at the correct size, however, when I pack the listbox, the whole window shrinks to the size of the listbox (and turns completely blue, which is expected).
In my experience, turning off geometry propagation is almost never the right solution. With a couple of decades of using tk and tkinter I've done that only two or three times for very specific edge cases.
Tkinter is very good at making the widget the best size based on the set of widgets you're using. Turning off propagation means you are responsible for doing all calculations to get the window to look right, and your calculations may not be correct when your program runs on a machine with different fonts or a different resolution. Tkinter can handle all of that for you.
Unfortunately, with such a small code example and without knowing your end goal it's hard to solve your layout problems. If your goal is to have a window that is 300x500, the best solution is to make the window that size and then have the frame fill the window, which is easier to do with pack than grid:
import tkinter as tk
w = tk.Tk() # New window
w.geometry("300x500")
f = tk.Frame(w, bg='red') # New frame with specific size
f.pack(fill="both", expand=True)
lb = tk.Listbox(f, bg='blue') # New listbox
lb.pack(fill=tk.BOTH, expand=True)
w.mainloop()
Thank you so much to Tls Chris for your answer and explanation. What I didn't realize is that grid_propagate() and pack_propagate() also affect a widget's children.
Therefore, with the help of Tls Chris, I have fixed my code and it now expands the listbox and doesn't shrink the frame.
Fixed code:
import tkinter as tk
w = tk.Tk() # New window
f = tk.Frame(w, width=300, height=500, bg='red') # New frame
f.grid_propagate(False) # Stopping things (that use grid) resizing the frame
f.pack_propagate(False) # Stopping things (that use pack) resizing the frame
f.grid(row=0, column=0)
lb = tk.Listbox(f, bg='blue') # New listbox
lb.pack(fill=tk.BOTH, expand=True) # Packing the listbox and making it expand and fill the frame
Edit: I think you actually want the listbox to expand to fit the frame. Amended the code to do that as an option
I haven't used pack much but I suspect that grid_propagate changes the behaviour of the grid geometry manager but not of the pack manager.
The below lets app() run with or without propagate set. It uses the grid geometry manager throughout.
import tkinter as tk
def app(propagate = False, expand = False ):
w = tk.Tk() # New window
tk.Label( w, text = 'Propagate: {} \nExpand: {}'.format(propagate, expand) ).grid()
f = tk.Frame(w, width=300, height=500, bg='red') # New frame with specific size
f.grid_propagate(propagate)
f.grid( row=1, column=0 )
lb = tk.Listbox(f, bg='blue') # New listbox
if expand:
f.columnconfigure(0, weight = 1 )
f.rowconfigure(0, weight = 1 )
# lb.pack(fill=tk.BOTH, expand=True)
lb.grid( row=0, column=0, sticky = 'nsew' )
# My guess is that grid_propagate has changed the behaviour of grid, not of pack.
lb.insert(tk.END, 'Test 1', 'Test 2', 'Test 3')
w.mainloop()
This changes the listbox geometry manager to grid. Run app() as below.
app(True, True) # propagate and Expand
app(False, True) # no propagate but expand
app(True, False) # propagate without expand
app() # no propagate or expand

Button Layout with Tkinter?

I want to make my buttons a 2 x 2 grid on the bottom of the window with a canvas above them but the buttons always seem to stack weird when I use .pack(side = whatever).
A major thing I also want the buttons and canvas to have relative size i.e. % so that whenever the window is resized the buttons still make up the right area.
I am not sure how to do this being new to tkinter and any help is appreciated.
import tkinter
from tkinter import *
code = Tk()
def LAttack():
print(something);
def HAttack():
print(something);
def FField():
print(something);
def Heal():
print(something);
def Restart():
print(something);
Attack1 = tkinter.Button(code,text = ("Light Attack"), command = LAttack)
Attack1.pack(side = LEFT)
Attack2 = tkinter.Button(code,text = ("Heavy Attack"), command = HAttack)
Attack2.pack(side = RIGHT)
Defense1 = tkinter.Button(code,text = ("Forcefield"), command = FField)
Defense1.pack(side = LEFT)
Defense2 = tkinter.Button(code,text = ("Heal"), command = Heal)
Defense2.pack(side = RIGHT)
Restart1 = tkinter.Button(code,text = ("Restart"), command = Restart)
Restart1.pack(side = TOP)
code.mainloop()
But I want it to look like this:
Mock up for GUI
I want to make my buttons a 2 x 2 grid on the bottom of the window with a canvas above them but the buttons always seem to stack weird when I use .pack(side = whatever).
To me this means that you clearly have two separate areas to be concerned with: a top area with a canvas, and a bottom area with buttons. The first step is to create those two areas. For the top just use the canvas, and for the bottom use a frame.
I'm going to assume you want the canvas to take up as much space as possible, with the buttons always on the bottom. For that sort of arrangement, pack makes the most sense.
The following gives us a program that has a canvas at the top and a frame at the bottom to hold the buttons. When you resize the window, the button frame remains at the bottom and the canvas fills the rest of the space:
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, background="white")
button_frame = tk.Frame(root)
button_frame.pack(side="bottom", fill="x", expand=False)
canvas.pack(side="top", fill="both", expand=True)
# <button code will be added here...>
root.mainloop()
Now we can focus on the buttons. You want the buttons in a 2x2 grid (though you have 5 buttons...?), so the natural choice is to use grid rather than pack. We want these buttons to be in the bottom frame, so we give that frame as the parent or master of the buttons.
You also somewhat curiously wrote "whenever the window is resized the buttons still make up the right area" even though earlier you said you wanted them on the bottom. I'm going to assume you mean that you want them in the bottom-right corner.
To accomplish this, we are going to create a grid with two rows and three columns. The column on the left will be empty, and it will take up any extra space to force the buttons to be on the right (of course, you can put things in this column if you wish)
This creates four buttons:
attack1 = tk.Button(button_frame, text="Light Attack")
attack2 = tk.Button(button_frame, text="Heavy Attack")
defense1 = tk.Button(button_frame, text="Forcefield")
defense2 = tk.Button(button_frame, text="Heal")
... this causes the first column to expand to fill any extra space:
button_frame.grid_columnconfigure(0, weight=1)
... and this lays them out in a grid (it's always good to separate widget creation from widget layout, because it makes it easier to visualize the layout in the code)
attack1.grid(row=0, column=1, sticky="ew")
attack2.grid(row=0, column=2, sticky="ew")
defense1.grid(row=1, column=1, sticky="ew")
defense2.grid(row=1, column=2, sticky="ew")
The end result is this:
When resized, the buttons retain their relative position:
Summary
The point of this is to show that you need to spend a few minutes organizing the items on the screen into logical groups. There are different layout tools for solving different problems (grid, pack, place), and most windows will benefit from using the right tool for each type of layout problem you're trying to solve (pack is good for horizontal and vertical layouts, grid is good for grids, and place covers a few edge cases where pixel-perfect control is needed).

Resize entry box in tkinter

The following MWE is for a window with horizontal and vertical scrollbars. The window contains an entry box in which the current working directory is displayed. However, the text in the entry box cannot all be seen as the box is too small. I would like to be able to display more of this text as the user enlarges the window. How can I adapt the following example so that the Entry box (defined in UserFileInput) resizes with the window? I have tried using window.grid_columnconfigure (see below), however this doesn't have any effect. It seems to be a problem with using the canvas, as previously I was able to get the Entry boxes to resize, however I need the canvas in order to place the horizontal and vertical scrollbars on the window.
window.grid(row=0, column=0, sticky='ew')
window.grid_columnconfigure(0, weight=1)
(and also with column = 1) but this doesn't have an effect.
import Tkinter as tk
import tkFileDialog
import os
class AutoScrollbar(tk.Scrollbar):
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
# grid_remove is currently missing from Tkinter!
self.tk.call("grid", "remove", self)
else:
self.grid()
tk.Scrollbar.set(self, lo, hi)
class Window(tk.Frame):
def UserFileInput(self,status,name):
row = self.row
optionLabel = tk.Label(self)
optionLabel.grid(row=row, column=0, sticky='w')
optionLabel["text"] = name
text = status#str(dirname) if dirname else status
var = tk.StringVar(root)
var.set(text)
w = tk.Entry(self, textvariable= var)
w.grid(row=row, column=1, sticky='ew')
w.grid_columnconfigure(1,weight=1)
self.row += 1
return w, var
def __init__(self,parent):
tk.Frame.__init__(self,parent)
self.row = 0
currentDirectory = os.getcwd()
directory,var = self.UserFileInput(currentDirectory, "Directory")
if __name__ == "__main__":
root = tk.Tk()
root.grid_columnconfigure(0, weight=1)
root.grid_rowconfigure(0,weight=1)
vscrollbar = AutoScrollbar(root,orient=tk.VERTICAL)
vscrollbar.grid(row=0, column=1, sticky='ns')
hscrollbar = AutoScrollbar(root, orient=tk.HORIZONTAL)
hscrollbar.grid(row=1, column=0, sticky='ew')
canvas=tk.Canvas(root,yscrollcommand=vscrollbar.set,xscrollcommand=hscrollbar.set)
canvas.grid(row=0, column=0, sticky='nsew')
vscrollbar.config(command=canvas.yview)
hscrollbar.config(command=canvas.xview)
window = Window(canvas)
canvas.create_window(0, 0, anchor=tk.NW, window=window)
window.update_idletasks()
canvas.config(scrollregion=canvas.bbox("all"))
root.mainloop()
You have several problems in your code that are getting in your way. The biggest obstacle is that you're putting a frame inside a canvas. That's rarely necessary, and it makes your code more complex than it needs to be? Is there a reason you're using a canvas, and is there a reason you're using classes for part of your code but not for everything?
Regardless, you have two problems:
the frame isn't growing when the window grows, so the contents inside the window can't grow
the label won't grow because you're using grid_columnconfigure incorrectly.
Hints for visualizing the problem
When trying to solve layout problems, a really helpful technique is to temporarily give each containing widget a unique color so you can see where each widget is. For example, coloring the canvas pink and the Window frame blue will make it clear that the Window frame is also not resizing.
Resizing the frame
Because you're choosing to embed your widget in a canvas, you are going to have to manually adjust the width of the frame when the containing canvas changes size. You can do that by setting a binding on the canvas to call a function whenever it resizes. The event you use for this is <Configure>. Note: the configure binding fires for more than just size changes, but you can safely ignore that fact.
The function needs to compute the width of the canvas, and thus the desired width of the frame (minus any padding you want). You'll then need to configure the frame to have that width. To facilitate that, you'll need to either keep a reference to the canvas id of the frame, or give the frame a unique tag.
Here is a function that assumes the frame has the tag "frame":
def on_canvas_resize(event):
padding = 8
width = canvas.winfo_width() - padding
canvas.itemconfigure("frame", width=width)
You'll need to adjust how you create the canvas item to include the tag:
canvas.create_window(..., tags=["frame"])
Finally, set a binding to fire when the widget changes size:
canvas.bind("<Configure>", on_canvas_resize)
Using grid_columnconfigure to get the label to resize
You need to use grid_columnconfigure on the containing widget. You want the columns inside the frame to grow and shrink, not the columns inside the label.
You need to change this line:
w.grid_columnconfigure(...)
to this:
self.grid_columnconfigure(...)

Categories

Resources