Skip to content

Instantly share code, notes, and snippets.

@JackTheEngineer
Last active February 10, 2024 16:26
Show Gist options
  • Save JackTheEngineer/81df334f3dcff09fd19e4169dd560c59 to your computer and use it in GitHub Desktop.
Save JackTheEngineer/81df334f3dcff09fd19e4169dd560c59 to your computer and use it in GitHub Desktop.
Tkinter python Scrolled Window / Frame / Canvas with Mousewheel support ( based upon EugeneBakin 's scrframe.py )
from tkinter import ttk
import tkinter as tk
import functools
fp = functools.partial
class VerticalScrolledFrame(ttk.Frame):
"""
A pure Tkinter scrollable frame that actually works!
* Use the 'interior' attribute to place widgets inside the scrollable frame
* Construct and pack/place/grid normally
* This frame only allows vertical scrolling
* -- NOTE: You will need to comment / uncomment code the differently for windows or linux
* -- or write your own 'os' type check.
* This comes from a different naming of the the scrollwheel 'button', on different systems.
"""
def __init__(self, parent, *args, **kw):
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
canvas.config(scrollregion="0 0 %s %s" % size)
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the canvas's width to fit the inner frame
canvas.config(width=interior.winfo_reqwidth())
def _configure_canvas(event):
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the inner frame's width to fill the canvas
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
"""
This is linux code for scrolling the window,
It has different buttons for scrolling the windows
"""
def _on_mousewheel(event, scroll):
canvas.yview_scroll(int(scroll), "units")
def _bind_to_mousewheel(event):
canvas.bind_all("<Button-4>", fp(_on_mousewheel, scroll=-1))
canvas.bind_all("<Button-5>", fp(_on_mousewheel, scroll=1))
def _unbind_from_mousewheel(event):
canvas.unbind_all("<Button-4>")
canvas.unbind_all("<Button-5>")
"""
This is windows code for scrolling the Frame
def _on_mousewheel(event):
canvas.yview_scroll(int(-1*(event.delta/120)), "units")
def _bind_to_mousewheel(event):
canvas.bind_all("<MouseWheel>", _on_mousewheel)
def _unbind_from_mousewheel(event):
canvas.unbind_all("<MouseWheel>")
"""
ttk.Frame.__init__(self, parent, *args, **kw)
# create a canvas object and a vertical scrollbar for scrolling it
vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=tk.FALSE)
canvas = tk.Canvas(self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.TRUE)
vscrollbar.config(command=canvas.yview)
# reset the view
canvas.xview_moveto(0)
canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.interior = interior = ttk.Frame(canvas)
interior_id = canvas.create_window(0, 0, window=interior,
anchor=tk.NW)
interior.bind('<Configure>', _configure_interior)
canvas.bind('<Configure>', _configure_canvas)
canvas.bind('<Enter>', _bind_to_mousewheel)
canvas.bind('<Leave>', _unbind_from_mousewheel)
# Thanks to chlutz214 for the usage update:
if __name__ == "__main__":
# Set Up root of app
root = tk.Tk()
root.geometry("400x500+50+50")
root.title("VerticalScrolledFrame Sample")
# Create a frame to put the VerticalScrolledFrame inside
holder_frame = tk.Frame(root)
holder_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.TRUE)
# Create the VerticalScrolledFrame
vs_frame = VerticalScrolledFrame(holder_frame)
vs_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=tk.TRUE)
# Fill the VerticalScrolledFrame
i = 0
while i != 100:
item = tk.Entry(vs_frame.interior)
item.insert(0, i)
item.pack(side=tk.TOP, fill=tk.X, expand=tk.TRUE)
i = i + 1
# Run mainloop to start app
root.mainloop()
@chlutz214
Copy link

chlutz214 commented Jan 7, 2021

Hi, thank you for this code and your effort. It would be helpful if you can provide an example of how one can use this class. For example to provide at the bottom of this code if name == 'main': and then the example.

I'm sure basfari figured this out already, but in case anyone else wants sample code (tested in 3.x):

if __name__ == "__main__":

        # Set Up root of app
        root = tk.Tk()
        root.geometry("400x500+50+50")
        root.title("VerticalScrolledFrame Sample")

        # Create a frame to put the VerticalScrolledFrame inside
        holder_frame = tk.Frame(root)
        holder_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.TRUE)

        # Create the VerticalScrolledFrame
        vs_frame = VerticalScrolledFrame(holder_frame)
        vs_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=tk.TRUE)

        # Fill the VerticalScrolledFrame
        i = 0
        while i != 100:
            item = tk.Entry(vs_frame.interior)
            item.insert(0, i)
            item.pack(side=tk.TOP, fill=tk.X, expand=tk.TRUE)
            i = i + 1

        # Run mainloop to start app
        root.mainloop()

@JackTheEngineer
Copy link
Author

Thank you @chlutz214 ! I've updated the gist, so it's copy paste - testable 👍

@Serhiy1
Copy link

Serhiy1 commented Jan 21, 2021

Hey, I modified this to use a more standard object initialisation pattern to make things slightly easier to read. As well as adding a platform check to the binding functions. A huge thanks for this code.

from tkinter import ttk
import tkinter as tk
import functools
fp = functools.partial

from sys import platform

class VerticalScrolledFrame(ttk.Frame):
    """
    A pure Tkinter scrollable frame that actually works!
    * Use the 'interior' attribute to place widgets inside the scrollable frame
    * Construct and pack/place/grid normally
    * This frame only allows vertical scrolling
    * This comes from a different naming of the the scrollwheel 'button', on different systems.
    """
    def __init__(self, parent, *args, **kw):

        super().__init__(parent, *args, **kw)

        # create a canvas object and a vertical scrollbar for scrolling it
        self.vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
        self.vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=tk.FALSE)
        self.canvas = tk.Canvas(self, bd=0, highlightthickness=0, yscrollcommand=self.vscrollbar.set)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.TRUE)
        self.vscrollbar.config(command=self.canvas.yview)

        # reset the view
        self.canvas.xview_moveto(0)
        self.canvas.yview_moveto(0)

        # create a frame inside the canvas which will be scrolled with it
        self.interior = ttk.Frame(self.canvas)
        self.interior_id = self.canvas.create_window(0, 0, window=self.interior,
                                           anchor=tk.NW)

        self.interior.bind('<Configure>', self._configure_interior)
        self.canvas.bind('<Configure>', self._configure_canvas)
        self.canvas.bind('<Enter>', self._bind_to_mousewheel)
        self.canvas.bind('<Leave>', self._unbind_from_mousewheel)
        
        
        # track changes to the canvas and frame width and sync them,
        # also updating the scrollbar

    def _configure_interior(self, event):
        # update the scrollbars to match the size of the inner frame
        size = (self.interior.winfo_reqwidth(), self.interior.winfo_reqheight())
        self.canvas.config(scrollregion="0 0 %s %s" % size)

        if self.interior.winfo_reqwidth() != self.winfo_width():
            # update the canvas's width to fit the inner frame
            self.canvas.config(width=self.interior.winfo_reqwidth())

    def _configure_canvas(self, event):
        if self.interior.winfo_reqwidth() != self.winfo_width():
            # update the inner frame's width to fill the canvas
            self.canvas.itemconfigure(self.interior_id, width=self.winfo_width())

    # This can now handle either windows or linux platforms
    def _on_mousewheel(self, event, scroll=None):

        if platform == "linux" or platform == "linux2":
            self.canvas.yview_scroll(int(scroll), "units")
        else:
            self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")

    def _bind_to_mousewheel(self, event):
        if platform == "linux" or platform == "linux2":
            self.canvas.bind_all("<MouseWheel>", fp(self._on_mousewheel, scroll=-1))
            self.canvas.bind_all("<Button-5>", fp(self._on_mousewheel, scroll=1))
        else:
            self.bind_all("<MouseWheel>", self._on_mousewheel)

    def _unbind_from_mousewheel(self, event):

        if platform == "linux" or platform == "linux2":
            self.canvas.unbind_all("<Button-4>")
            self.canvas.unbind_all("<Button-5>")
        else:
            self.unbind_all("<MouseWheel>")


# Thanks to chlutz214 for the usage update:
if __name__ == "__main__":
        # Set Up root of app
        root = tk.Tk()
        root.geometry("400x500+50+50")
        root.title("VerticalScrolledFrame Sample")

        # Create a frame to put the VerticalScrolledFrame inside
        holder_frame = tk.Frame(root)
        holder_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.TRUE)

        # Create the VerticalScrolledFrame
        vs_frame = VerticalScrolledFrame(holder_frame)
        vs_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=tk.TRUE)

        for index in range(100):
            item = tk.Entry(vs_frame.interior)
            item.insert(0, index)
            item.pack(side=tk.TOP, fill=tk.X, expand=tk.TRUE)

        # Run mainloop to start app
        root.mainloop()
 

@Letouane
Copy link

Letouane commented May 5, 2022

Thank you for the code, succeeded to use it in a non object oriented script. It runs smoothly!

@3n4rch3
Copy link

3n4rch3 commented May 9, 2022

@Serhiy1
Just a note: in the _bind_to_mousewheel function, first linux platform line, swap out "<MouseWheel>" to "<Button-4>" (typo?)...and it just works!
Thanks, really appreciate you all sharing your work!

@chazer
Copy link

chazer commented Apr 21, 2023

Also MouseWhell events on Darwin(MacOS) has dynamic delta values
Works well smoothly on touchpad 😀

        def _on_mousewheel(event, scroll=None):
            os = platform.system()
            if os == 'Windows':
                canvas.yview_scroll(int(-1*(event.delta/120)), "units")
            elif os == 'Darwin':
                canvas.yview_scroll(int(-1 * event.delta), "units")
            else:
                canvas.yview_scroll(int(scroll), "units")

        def _bind_to_mousewheel(event):
            os = platform.system()
            if os == 'Windows':
                canvas.bind_all("<MouseWheel>", _on_mousewheel)
            elif os == 'Darwin':
                canvas.bind_all("<MouseWheel>", _on_mousewheel)
            else:
                canvas.bind_all("<Button-4>", fp(_on_mousewheel, scroll=-1))
                canvas.bind_all("<Button-5>", fp(_on_mousewheel, scroll=1))

        def _unbind_from_mousewheel(event):
            os = platform.system()
            if os == 'Windows':
                canvas.unbind_all("<MouseWheel>")
            elif os == 'Darwin':
                canvas.unbind_all("<MouseWheel>")
            else:
                canvas.unbind_all("<Button-4>")
                canvas.unbind_all("<Button-5>")

@thududoo
Copy link

I copied the code and ran on my mac, it seems only when my pointer is on the scroll bar the mousewheel would work, if it's on the frame it self it doesn't, tried multiple modifications but can't find any fix :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment