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