Reputation: 4726
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
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