4

I'm trying to create a scrollable Python Tkinter widget that can contain other widgets. The following dummy code (lifted mostly from the answer to this SO question, and then adapted to suit my style) seems to do almost exactly what I want it to do:

import Tkinter as tk

class ScrollFrame(tk.Frame):
    def __init__(self, root, *args, **kwargs):
        # Start up self
        tk.Frame.__init__(self, root, *args, **kwargs)
        # Put a canvas in the frame (self), along with scroll bars
        self.canvas = tk.Canvas(self)
        self.horizontal_scrollbar = tk.Scrollbar(
            self, orient="horizontal", command=self.canvas.xview
            )
        self.vertical_scrollbar = tk.Scrollbar(
            self, orient="vertical", command=self.canvas.yview
            )
        self.canvas.configure(
            yscrollcommand=self.vertical_scrollbar.set,
            xscrollcommand=self.horizontal_scrollbar.set
            )
        # Put a frame in the canvas, to hold all the widgets
        self.inner_frame = tk.Frame(self.canvas)
        # Pack the scroll bars and the canvas (in self)
        self.horizontal_scrollbar.pack(side="bottom", fill="x")
        self.vertical_scrollbar.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)
        self.canvas.create_window((0,0), window=self.inner_frame, anchor="nw")
        self.inner_frame.bind("<Configure>", self.OnFrameConfigure)

    def OnFrameConfigure(self, event):
        '''Reset the scroll region to encompass the inner frame'''
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

root = tk.Tk()
frame = ScrollFrame(root, borderwidth=2, relief="sunken")
labels = []
for i in range(10):
    labels.append(
        tk.Label(
            frame.inner_frame, text="Row {}".format(i) + "_"*20
            ) # Unfortunately, this widget's parent cannot just be frame but has to be frame.inner_frame
        )
frame.place(x=20, y=20, width=150, height=150)

for i,label in enumerate(labels):
    label.grid(row=i,column=0)
    #label.place(x=0, y=20*i, width=100, height=20)
root.mainloop()

(There are 10 labels, each of which has some spam at the end, to test if the vertical and horizontal scrolling both work.)

However, for my actual application, I need very fine control over where each widget ends up, which forces me to use the place geometry manager instead of grid. (Notice that I place()d the frame.) However, if I replace the line which grid()s each label with a line that uses place(), the labels don't get displayed any more at all. (In above code, emulate this by commenting out the third line from the bottom, and uncommenting the second line from the bottom.)

Why doesn't this work? How can I fix it?


EDIT: The accepted answer led me to the following code, which works as intended, by passing in an inner_width and inner_height to the initialization of the ScrollFrame class, which then get passed as the width and height parameters of the inner_frame:

import Tkinter as tk

class ScrollFrame(tk.Frame):
    def __init__(self, root, inner_width, inner_height, *args, **kwargs):
        # Start up self
        tk.Frame.__init__(self, root, *args, **kwargs)
        # Put a canvas in the frame (self)
        self.canvas = tk.Canvas(self)
        # Put scrollbars in the frame (self)
        self.horizontal_scrollbar = tk.Scrollbar(
            self, orient="horizontal", command=self.canvas.xview
            )
        self.vertical_scrollbar = tk.Scrollbar(
            self, orient="vertical", command=self.canvas.yview
            )
        self.canvas.configure(
            yscrollcommand=self.vertical_scrollbar.set,
            xscrollcommand=self.horizontal_scrollbar.set
            )
        # Put a frame in the canvas, to hold all the widgets
        self.inner_frame = tk.Frame(
            self.canvas, width=inner_width, height=inner_height
            )
        # Pack the scroll bars and the canvas (in self)
        self.horizontal_scrollbar.pack(side="bottom", fill="x")
        self.vertical_scrollbar.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)
        self.canvas.create_window((0,0), window=self.inner_frame, anchor="nw")
        self.inner_frame.bind("<Configure>", self.on_frame_configure)

    def on_frame_configure(self, event):
        """Reset the scroll region to encompass the inner frame"""
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))


root = tk.Tk()
frame = ScrollFrame(root, 150, 200, borderwidth=2, relief="sunken")
labels = []
for i in range(10):
    labels.append(
        tk.Label(
            frame.inner_frame, text="Row {}".format(i) + "_"*20, anchor="nw"
            ) # Unfortunately, this widget's parent cannot just be frame but has to be frame.inner_frame
        )
frame.place(x=20, y=20, width=150, height=150)

for i,label in enumerate(labels):
    #label.grid(row=i,column=0)
    label.place(x=0, y=20*i, width=100, height=20)
root.mainloop()
Community
  • 1
  • 1
acdr
  • 4,538
  • 2
  • 19
  • 45
  • You can have perfect control where widgets are place with the `grid` layout manager, you just need to know how it works. I really discourage the use of `place`, maybe because I have never used it (maybe just once), but anyway this is not a solution to your question, but just an advice (maybe)... – nbro Jun 24 '15 at 14:22
  • @Christopher Yeah, I'm aware that place() is discouraged in general when using Tkinter. I don't know if I can use grid() without causing weirdness, but even if I can, it would take many hours to convert all of my UI-related code, which I'd prefer not to do. :) – acdr Jun 24 '15 at 14:28
  • 1
    @ChristopherWallace: that's not true -- you don't have perfect control over where widgets are place with grid, unless you take it to extremes and create a layout that has one grid cell for every pixel. – Bryan Oakley Jun 24 '15 at 14:30
  • 1
    Is using a frame inside the canvas a must? Canvas objects have a method called create_window, which takes another widget and "puts it inside of it". – Vicyorus Jun 24 '15 at 14:32
  • @BryanOakley For most of the purposes, you have the necessary and sufficient control using `grid`. We might have more control using `place`, but the result in my opinion is not good for most of the tasks. To be honest, I don't know what the OP really wants to achieve, and `place` could be the right choice... – nbro Jun 24 '15 at 20:50
  • 2
    @ChristopherWallace: Oh, I agree! You do have "necessary and sufficient control". However, your statement was that you have "perfect" control with grid, which I don't think is accurate. `place` gives you absolute ("perfect") control, `grid` and `pack` both are considerably more convenient, and almost always yield better results. I never recommend `place` except for the most unusual of circumstances. Over a couple decades of tk use, I've used `place` maybe 3-4 times. – Bryan Oakley Jun 24 '15 at 21:56

1 Answers1

6

It is because place won't resize the parent window to accommodate child widgets. You need to explicitly set the size of the parent frame when using place. Your labels are there, but the frame has its default size of 1x1 pixels, effectively making the labels invisible.

From the official documentation for place:

Unlike many other geometry managers (such as the packer) the placer does not make any attempt to manipulate the geometry of the master windows or the parents of slave windows (i.e. it does not set their requested sizes). To control the sizes of these windows, make them windows like frames and canvases that provide configuration options for this purpose.

If all you need to do is manage a bunch of widgets on a scrollable canvas with absolute control, just use the canvas method create_window, which will let you put labels directly on the canvas at precise coordinates. This is much easier than using place to put the widgets in a frame, and then putting the frame in the canvas.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Accepted! I'm not going to follow your suggestion of using `create_window` (simply so I can keep consistency with other widgets, which do use `place`). Instead, in instantiating the inner frame, I pass an inner_width and inner_height argument to the ScrollFrame class, which are then passed to the inner frame's width and height arguments. (I.e. when creating a ScrollFrame, you specify how large the area is through which you can scroll.) – acdr Jun 24 '15 at 15:07