1

I work on tkinter with a scrollable frame class provided here : https://gist.github.com/mp035/9f2027c3ef9172264532fcd6262f3b01

The actual code for this class is :

class ScrollFrame(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent) # create a frame (self)

        s=ttk.Style()
        s.configure('TFrame', background="#eff0f1")

        #place canvas on self
        self.canvas = tk.Canvas(self, borderwidth=0, background="#eff0f1", height = appHeight)
        #place a frame on the canvas, this frame will hold the child widgets
        self.viewPort = ttk.Frame(self.canvas, style='TFrame')
        #place a scrollbar on self
        self.vsb = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        #attach scrollbar action to scroll of canvas
        self.canvas.configure(yscrollcommand=self.vsb.set)

        #pack scrollbar to right of self
        self.vsb.pack(side="right", fill="y")
        #pack canvas to left of self and expand to fil
        self.canvas.pack(side="left", fill="both", expand=True)
        self.canvas_window = self.canvas.create_window((4,4),
                                                 #add view port frame to canvas
                                                 window=self.viewPort, anchor="nw",
                                                 tags="self.viewPort")

        #bind an event whenever the size of the viewPort frame changes.
        self.viewPort.bind("<Configure>", self.onFrameConfigure)
        #bind an event whenever the size of the viewPort frame changes.
        self.canvas.bind("<Configure>", self.onCanvasConfigure)

        #perform an initial stretch on render, otherwise the scroll region has a tiny border until the first resize
        self.onFrameConfigure(None)

    def onFrameConfigure(self, event):
        '''Reset the scroll region to encompass the inner frame'''
        #whenever the size of the frame changes, alter the scroll region respectively.
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def onCanvasConfigure(self, event):
        '''Reset the canvas window to encompass inner frame when required'''
        canvas_width = event.width
        #whenever the size of the canvas changes alter the window region respectively.
        self.canvas.itemconfig(self.canvas_window, width = canvas_width)

It actually works pretty good except I can't make mousewheel works on the scrollbar... I have to click on the slider and make it slide. Whenever I try to scroll with the mousewheel, I got this error in the console:

Traceback (most recent call last):
  File "D:\Anaconda\lib\tkinter\__init__.py", line 1705, in __call__
    return self.func(*args)
  File "D:\Anaconda\lib\tkinter\__init__.py", line 1739, in yview
    res = self.tk.call(self._w, 'yview', *args)
_tkinter.TclError: unknown option "": must be moveto or scroll

I've found this topic : tkinter: binding mousewheel to scrollbar

but none of the given answer to adapt my class seemed to work.

Does anyone knows how can I adapt my scrollable frame in order to make the mousewheel works ? Thanks in advance !!

j_4321
  • 15,431
  • 3
  • 34
  • 61
Jb Melmi
  • 41
  • 5

1 Answers1

1

Ok, I found the answer so I post here the solution:

at the end of the init, I added this:

        self.viewPort.bind('<Enter>', self._bound_to_mousewheel)
        self.viewPort.bind('<Leave>', self._unbound_to_mousewheel)

Then right after I added this:

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

I can now scroll the bar with the mousewheel wherever is the pointer !

So here is the fully working scrollable frame class code :

class ScrollFrame(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent) # create a frame (self)

        s=ttk.Style()
        s.configure('TFrame', background="#eff0f1")

        #place canvas on self
        self.canvas = tk.Canvas(self, borderwidth=0, background="#eff0f1", height = appHeight)
        #place a frame on the canvas, this frame will hold the child widgets
        self.viewPort = ttk.Frame(self.canvas, style='TFrame')
        #place a scrollbar on self
        self.vsb = ttk.Scrollbar(self, orient="vertical")
        #attach scrollbar action to scroll of canvas
        self.canvas.configure(yscrollcommand=self.vsb.set)

        #pack scrollbar to right of self
        self.vsb.pack(side="right", fill="y")
        #pack canvas to left of self and expand to fil
        self.canvas.pack(side="left", fill="both", expand=True)
        self.canvas_window = self.canvas.create_window((4,4),
                                                 #add view port frame to canvas
                                                 window=self.viewPort, anchor="nw",
                                                 tags="self.viewPort")

        #bind an event whenever the size of the viewPort frame changes.
        self.viewPort.bind("<Configure>", self.onFrameConfigure)
        #bind an event whenever the size of the viewPort frame changes.
        self.canvas.bind("<Configure>", self.onCanvasConfigure)

        #perform an initial stretch on render, otherwise the scroll region has a tiny border until the first resize
        self.onFrameConfigure(None)

        self.viewPort.bind('<Enter>', self._bound_to_mousewheel)
        self.viewPort.bind('<Leave>', self._unbound_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")

    def onFrameConfigure(self, event):
        '''Reset the scroll region to encompass the inner frame'''
        #whenever the size of the frame changes, alter the scroll region respectively.
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def onCanvasConfigure(self, event):
        '''Reset the canvas window to encompass inner frame when required'''
        canvas_width = event.width
        #whenever the size of the canvas changes alter the window region respectively.
        self.canvas.itemconfig(self.canvas_window, width = canvas_width)

All you have to do when you want to create a page in tkinter is to call it this way:

class NewPage(ttk.Frame):
    def __init__(self, parent, controller):

        ttk.Frame.__init__(self, parent)

        self.scrollFrame = ScrollFrame(self)

and if you want to add widgets in it, just buil them with 'self.scrollFrame.viewPort' like this:

label = ttk.Label(self.scrollFrame.viewPort, text = "Label", anchor = "center")

Hope it'll help someone ;)

Jb Melmi
  • 41
  • 5
  • btw, it would be great if You would add the language after backticks so in this case like this: `\```python` and then 3 backticks at the end as You do, I will edit so You can take a look, it just colors the syntax, also why do You have `return None` at the end of `__init__`, it has no use and can be removed – Matiiss Jul 13 '21 at 13:15
  • Oh nice, I didn't know I could do that ! Thanks for this, it's much more readable indeed. Oh you right for the ```return None```. I guess it remains from a previous attempt... I eddited my answer to remove it. Thanks for that too :) – Jb Melmi Jul 13 '21 at 13:24