Reputation: 1123
EDIT [resolved]
based on the answer from Thingamabobs below, the approach simply turns out to be making sure the elements you spawn are allocated to correct parents.
It is worthwhile to create a frame just to hold every scrollable element since you can't move widgets around between parents when using pack/place
. So a common parents between all movable
elements will give you the freedom to move them around, again just create a holder frame. See the answer and discussion below for more details.
EDIT2
Bryan's answer below has very good info and an alternate approach using just a canvas. The core concepts still stand the same.
original question begins here
So based on this answer by Bryan, I used this approach to spawn widgets on a scrollable frame
(which is basically a Canvas
wrapping a frame inside as we know cause Frames don't have scroll attributes).
The widgets in this case are just tk.Button
s which I spawn in a loop using lambda to preserve state. That aspect is completely fine.
Now everything works fine except when I spawn more elements (again just buttons), they seem to be cut off. and I can't seem to scroll down to see the remaining ones. I am only able to scroll upwards only to see empty space.
please see the video below to see what I mean (excuse my horrible color choices, this is for a quick demo)
In the video, there are more elements below template47
but I can not scroll to them. I can scroll upwards but it's just lonely up there. All the names are actually buttons with zero bd
and HLthickness
.
To begin, my first instinct was to attach a ttk.scrollbar
to the canvas+frame
, which I did but observed the exact same behavior.
Then I tried to see if i could use .moveTo('1.0')
to scroll down to last entry and then since upward scrolling works already, shouldn't have an issue. But this didn't do anything either. Still the elements were cut off, and it obviously messed up upward scrolling too.
I don't think I can use pack/grid
geoManagers since as the answer by bryan i linked to above suggests, using place
with its in_
arg is the preferred way. If it is possible otherwise, let me know though.
as depicted in the answer linked above, I also have two frames, and I'm using the on_click callback function to switch parents (a lot like in example code in answer). Turned out place was the best bet to achieve this. and it is, all of that aspect is working well. It's just the scroll thingy which doesn't work.
how i bind to mousewheel
def _bound_to_mousewheel(self, event):
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
def _unbound_to_mousewheel(self, event):
self.canvas.unbind_all("<MouseWheel>")
def _on_mousewheel(self, event):
self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
creating the window
self.canvas.create_window((5, 6), window=self.scrolled_frame, anchor="w")
ad of course the widgets inside
for f in self.fav_templates:
btn = tk.Button(self.root, text='text', command=lambda te=f: self._on_click(te))
btn.place(in_=self.ft_frame, x=20, y=p)
I'm sure above snippets would give idea of what I've done. If someone needs me to create a complete MCVE they can run, happy to do that, lemme know.
What did I miss in the setup? any ideas or suggestion are welcome.
Thank You :)
Upvotes: 4
Views: 992
Reputation: 385870
If you're creating a vertical stack of widgets, you're overcomplicating the solution when you embed a frame in a canvas. Instead, you can create the buttons directly in the canvas. The only time an embedded frame is useful is when you want to leverage the power of pack
or grid
to arrange the widgets into a complex layout. In this case you're creating a vertical stack, so the frame is unnecessary.
Here are a few things to notice about the following code:
the buttons are a child of the canvas, not a child of root. I don't know why you were making the buttons the child of the root window since they are going in the canvas. They need to be a child of the canvas so that they don't draw outside of the border of the canvas.
a property named lasty
will return the bottom y coordinate. Every new button will get added below this coordinate.
this code doesn't use self.fav_templates
for the sake of brevity.
import tkinter as tk
class ScrolledButtons(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.canvas = tk.Canvas(self)
self.vsb = tk.Scrollbar(self, command=self.canvas.yview, orient="vertical")
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
for n in range(20):
x = 2
y = self.lasty + 2
btn = tk.Button(self.canvas, text=f'Button #{n}', command=lambda te=n: self._on_click(te))
self.canvas.create_window(20, y, anchor="nw", window=btn)
bbox = self.canvas.bbox("all")
self.canvas.configure(scrollregion=(0, 0, bbox[2], bbox[3]))
@property
def lasty(self):
bbox = self.canvas.bbox("all")
lasty = bbox[3] if bbox else 0
return lasty
root = tk.Tk()
w = ScrolledButtons(root)
w.pack(fill="both", expand=True)
root.mainloop()
Upvotes: 1
Reputation: 8037
The main issue is that you are using place
and with place you will be the allmighty over the widget, means there will be no requested size to the master or any other magic tkinter provides in the background. So I do recommand to use another geometry manager that does that magic for you like pack. Also note that I set the anchor to nw
.
In addition it appears that you can only use the optional argument in_
in a common master. So the key of that concept is to have an holder frame that is used as master parameter and use the in_
for children that are able to hold widgets.
import tkinter as tk
class Example:
def __init__(self):
self.root = tk.Tk()
test_frame = tk.Frame(self.root,height=200,width=400,bg='orange')
test_frame.pack()
self.DISPLAY_WIDTH = 200
self.DISPLAY_HEIGHT= 200
self.create_holder()
self.create_displays()
self.add_scrollbars()
self.populate_holder()
def create_holder(self):
'''creates a canvas for the displays and is needed
to have a common border/window'''
self.holder = tk.Canvas(self.root,width=self.DISPLAY_WIDTH*2,height=self.DISPLAY_HEIGHT)
self.holder_frame = tk.Frame(self.holder)
self.holder.create_window((5, 6), window=self.holder_frame, anchor="nw")
self.holder.pack()
def create_displays(self):
'''creates 2 displays to have seperate scrollregions'''
self.cnvs1 = tk.Canvas(self.holder_frame,
width=self.DISPLAY_WIDTH,
height=self.DISPLAY_HEIGHT)
self.cnvs2 = tk.Canvas(self.holder_frame,
width=self.DISPLAY_WIDTH,
height=self.DISPLAY_HEIGHT)
self.lf1 = tk.Frame(self.cnvs1);self.cnvs1.create_window((5, 6), window=self.lf1, anchor="nw")
self.lf2 = tk.Frame(self.cnvs2);self.cnvs2.create_window((5, 6), window=self.lf2, anchor="nw")
def add_scrollbars(self):
self.vsb1 = tk.Scrollbar(self.holder_frame, orient="vertical", command=self.cnvs1.yview)
self.vsb2 = tk.Scrollbar(self.holder_frame, orient="vertical", command=self.cnvs2.yview)
self.cnvs1.configure(yscrollcommand=self.vsb1.set)
self.lf1.bind("<Configure>", self.onFrameConfigure)
self.cnvs2.configure(yscrollcommand=self.vsb2.set)
self.lf2.bind("<Configure>", self.onFrameConfigure)
def populate_holder(self):
self.cnvs1.pack(side="left", fill="both", expand=True)
self.vsb1.pack(side="left", fill="y")
self.cnvs2.pack(side="right", fill="both", expand=True)
self.vsb2.pack(side="right", fill="y")
for i in range(20):
button = tk.Button(self.holder_frame,text="Click me")
button.config(command=lambda b=button:self.on_click(b))
button.pack(in_=self.lf1)
def start(self):
self.root.mainloop()
def onFrameConfigure(self, event):
self.cnvs1.configure(scrollregion=self.cnvs1.bbox("all"))
self.cnvs2.configure(scrollregion=self.cnvs2.bbox("all"))
def on_click(self,button):
current_frame = button.pack_info().get("in")
new_frame = self.lf1 if current_frame == self.lf2 else self.lf2
button.pack(in_=new_frame)
Example().start()
Upvotes: 1