Reputation: 1901
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!
Upvotes: 1
Views: 329
Reputation: 385870
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.
Upvotes: 2