acdr
acdr

Reputation: 4726

Why does place() not work for widgets in a Frame in a Canvas?

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()

Upvotes: 4

Views: 2882

Answers (1)

Bryan Oakley
Bryan Oakley

Reputation: 386295

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.

Upvotes: 6

Related Questions