J-L
J-L

Reputation: 1901

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

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

Answers (1)

Bryan Oakley
Bryan Oakley

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

Related Questions