-
-
Save JackTheEngineer/81df334f3dcff09fd19e4169dd560c59 to your computer and use it in GitHub Desktop.
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() | |
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()
Thank you @chlutz214 ! I've updated the gist, so it's copy paste - testable 👍
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()
Thank you for the code, succeeded to use it in a non object oriented script. It runs smoothly!
@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!
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>")
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 :(
In Serhiy1's code replace:
if platform == "linux" or platform == "linux2":
self.canvas.bind_all("<MouseWheel>", fp(self._on_mousewheel, scroll=-1))
with
if platform == "linux" or platform == "linux2":
self.canvas.bind_all("<Button-4>", fp(self._on_mousewheel, scroll=-1))
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.