P S Solanki
P S Solanki

Reputation: 1123

tkinter scrollbar only scrolls downwards and cuts off content

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


The situation

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.Buttons which I spawn in a loop using lambda to preserve state. That aspect is completely fine.

The issue

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)

enter image description here

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.

what I have tried

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.

my use case in a nutshell

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.

some code (dare i say MCVE)

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)

So

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

Answers (2)

Bryan Oakley
Bryan Oakley

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

screenshot

Upvotes: 1

Thingamabobs
Thingamabobs

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

Related Questions