1

I would like to make a Tkinter class, based on the answer here, which is a Frame that automatically shows/hides Scrollbars around the content as necessary.

The answer that I linked to above works perfectly for my needs, with the caveat that because it's not contained within a class, it's not reusable. I figured this would be pretty quick and easy, but for some reason, my AutoScrollbars never appear once I refactor the code into its own class, regardless of how much or little of the contents of the Frame is hidden by the window resizing.

# This class is unchanged from the other answer that I linked to,
# but I'll reproduce its source code below for convenience.
from autoScrollbar import AutoScrollbar
from Tkinter       import Button, Canvas, Frame, HORIZONTAL, Tk, VERTICAL

# This is the class I made - something isn't right with it.
class AutoScrollable(Frame):
    def __init__(self, top, *args, **kwargs):
        Frame.__init__(self, top, *args, **kwargs)

        hscrollbar = AutoScrollbar(self, orient = HORIZONTAL)
        hscrollbar.grid(row = 1, column = 0, sticky = 'ew')

        vscrollbar = AutoScrollbar(self, orient = VERTICAL)
        vscrollbar.grid(row = 0, column = 1, sticky = 'ns')

        canvas = Canvas(self, xscrollcommand = hscrollbar.set,
                              yscrollcommand = vscrollbar.set)
        canvas.grid(row = 0, column = 0, sticky = 'nsew')

        hscrollbar.config(command = canvas.xview)
        vscrollbar.config(command = canvas.yview)

        # Make the canvas expandable
        self.grid_rowconfigure(0, weight = 1)
        self.grid_columnconfigure(0, weight = 1)

        # Create the canvas contents
        self.frame = Frame(canvas)
        self.frame.rowconfigure(1, weight = 1)
        self.frame.columnconfigure(1, weight = 1)

        canvas.create_window(0, 0, window = self.frame, anchor = 'nw')
        canvas.config(scrollregion = canvas.bbox('all'))

# This is an example of using my new class I defined above.
# It's how I know my class isn't working quite right.
root = Tk()
autoScrollable = AutoScrollable(root)
autoScrollable.grid(row = 0, column = 0, sticky = 'news')
root.rowconfigure(0, weight = 1)
root.columnconfigure(0, weight = 1)

for i in xrange(10):
    for j in xrange(10):
        button = Button(autoScrollable.frame, text = '%d, %d' % (i, j))
        button.grid(row = i, column = j, sticky = 'news')

autoScrollable.frame.update_idletasks()

root.mainloop()

Here's the source for autoScrollbar, which I'm including because I import it in the above source, but I don't think the actual problem is here.

# Adapted from here: http://effbot.org/zone/tkinter-autoscrollbar.htm

from Tkinter import Scrollbar

class AutoScrollbar(Scrollbar):
    '''
    A scrollbar that hides itself if it's not needed. 
    Only works if you use the grid geometry manager.
    '''
    def set(self, lo, hi):
        if float(lo) <= 0.0 and float(hi) >= 1.0:
            self.grid_remove()
        else:
            self.grid()
        Scrollbar.set(self, lo, hi)

    def pack(self, *args, **kwargs):
        raise TclError('Cannot use pack with this widget.')

    def place(self, *args, **kwargs):
        raise TclError('Cannot use pack with this widget.')
Community
  • 1
  • 1
ArtOfWarfare
  • 20,617
  • 19
  • 137
  • 193

1 Answers1

2

You're calling canvas.config(scrollregion = canvas.bbox('all')) when the canvas is still empty, effectively making the scrollregion (0, 0, 1, 1).

You should wait with defining the scrollregion until you have the widgets in your Frame. To do that you should rename canvas to self.canvas in your AutoScrollable class and call

autoScrollable.canvas.config(scrollregion = autoScrollable.canvas.bbox('all'))

right after

autoScrollable.frame.update_idletasks()

You could also bind a <Configure> event to your autoScrollable.frame which calls both the update_idletasks() and updates the scrollregion. That way, you don't have to worry about calling it yourself anymore because they update whenever the Frame's size is changed.

    self.frame.bind('<Configure>', self.frame_changed)

def frame_changed(self, event):
    self.frame.update_idletasks()
    self.canvas.config(scrollregion = self.canvas.bbox('all'))
fhdrsdg
  • 10,297
  • 2
  • 41
  • 62
  • This sounds promising. I'll try making some of the changes you suggested tonight and let you know if they worked out or not (and if they do work I'll accept and upvote.) – ArtOfWarfare May 04 '15 at 13:48
  • Great! I tested it and it worked perfectly. I made a few tiny changes though. I defined `frameChanged` within `__init__` so that the function wouldn't be visible. That meant dropping the `self` argument (it's now an inner function, not a method). I changed the `event` argument to `_` to indicate it's intentionally unused. I didn't make `canvas` a property of `AutoScrollable`, so dropped all `self.`s referencing it - no need to expose it outside the class. – ArtOfWarfare May 05 '15 at 22:50
  • Mac users take note: when using `self.frame.bind(''), self.frame_changed)` and the frame_changed method, buttons in the frame containing the autoScrollable frame (e.g., in root, etc.) do not show on Mac. They do, however, show on linux. (Since this was duplicate files I don't think it's due to a mistake on my part, and I've seen other mentions of buttons not showing on Mac.) Using the first suggestion by @fhdrsdg using `autoScrollable.canvas.config(scrollreagion = autoScrollable.canvas.bbox('all'))` outside of the AutoScrollable class works perfectly on both. Thanks both of you! – fitzl Dec 23 '18 at 16:51