Skip to content

Instantly share code, notes, and snippets.

@novel-yet-trivial
Last active January 9, 2024 01:10
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save novel-yet-trivial/3eddfce704db3082e38c84664fc1fdf8 to your computer and use it in GitHub Desktop.
Save novel-yet-trivial/3eddfce704db3082e38c84664fc1fdf8 to your computer and use it in GitHub Desktop.
A vertical scrolled frame for python tkinter that behaves like a normal Frame. Tested with python 2 and 3, windows and linux.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class VerticalScrolledFrame:
"""
A vertically scrolled Frame that can be treated like any other Frame
ie it needs a master and layout and it can be a master.
:width:, :height:, :bg: are passed to the underlying Canvas
:bg: and all other keyword arguments are passed to the inner Frame
note that a widget layed out in this frame will have a self.master 3 layers deep,
(outer Frame, Canvas, inner Frame) so
if you subclass this there is no built in way for the children to access it.
You need to provide the controller separately.
"""
def __init__(self, master, **kwargs):
width = kwargs.pop('width', None)
height = kwargs.pop('height', None)
bg = kwargs.pop('bg', kwargs.pop('background', None))
self.outer = tk.Frame(master, **kwargs)
self.vsb = tk.Scrollbar(self.outer, orient=tk.VERTICAL)
self.vsb.pack(fill=tk.Y, side=tk.RIGHT)
self.canvas = tk.Canvas(self.outer, highlightthickness=0, width=width, height=height, bg=bg)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.canvas['yscrollcommand'] = self.vsb.set
# mouse scroll does not seem to work with just "bind"; You have
# to use "bind_all". Therefore to use multiple windows you have
# to bind_all in the current widget
self.canvas.bind("<Enter>", self._bind_mouse)
self.canvas.bind("<Leave>", self._unbind_mouse)
self.vsb['command'] = self.canvas.yview
self.inner = tk.Frame(self.canvas, bg=bg)
# pack the inner Frame into the Canvas with the topleft corner 4 pixels offset
self.canvas.create_window(4, 4, window=self.inner, anchor='nw')
self.inner.bind("<Configure>", self._on_frame_configure)
self.outer_attr = set(dir(tk.Widget))
def __getattr__(self, item):
if item in self.outer_attr:
# geometry attributes etc (eg pack, destroy, tkraise) are passed on to self.outer
return getattr(self.outer, item)
else:
# all other attributes (_w, children, etc) are passed to self.inner
return getattr(self.inner, item)
def _on_frame_configure(self, event=None):
x1, y1, x2, y2 = self.canvas.bbox("all")
height = self.canvas.winfo_height()
self.canvas.config(scrollregion = (0,0, x2, max(y2, height)))
def _bind_mouse(self, event=None):
self.canvas.bind_all("<4>", self._on_mousewheel)
self.canvas.bind_all("<5>", self._on_mousewheel)
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
def _unbind_mouse(self, event=None):
self.canvas.unbind_all("<4>")
self.canvas.unbind_all("<5>")
self.canvas.unbind_all("<MouseWheel>")
def _on_mousewheel(self, event):
"""Linux uses event.num; Windows / Mac uses event.delta"""
if event.num == 4 or event.delta > 0:
self.canvas.yview_scroll(-1, "units" )
elif event.num == 5 or event.delta < 0:
self.canvas.yview_scroll(1, "units" )
def __str__(self):
return str(self.outer)
# **** SCROLL BAR TEST *****
if __name__ == "__main__":
root = tk.Tk()
root.title("Scrollbar Test")
root.geometry('400x500')
frame = VerticalScrolledFrame(root,
width=300,
borderwidth=2,
relief=tk.SUNKEN,
background="light gray")
#frame.grid(column=0, row=0, sticky='nsew') # fixed size
frame.pack(fill=tk.BOTH, expand=True) # fill window
for i in range(30):
label = tk.Label(frame, text="This is a label "+str(i))
label.grid(column=1, row=i, sticky=tk.W)
text = tk.Entry(frame, textvariable="text")
text.grid(column=2, row=i, sticky=tk.W)
root.mainloop()
@cxr00
Copy link

cxr00 commented Mar 22, 2022

This was exactly what I needed. I put a link to the original code in the comments of my project.

@King-of-Kings-980
Copy link

@streanger your answer is great but you did a mistake: In line 61 instead of self.canvas.itemconfigure("all", width=width, height=height) it should be self.canvas.itemconfigure("all", width=width) (without , height=height). Otherwise you won't be able to scroll correctly.

@streanger
Copy link

@King-of-Kings-980 you're absolutely right. Now i realized that in my private code I got correct version however forget to update here. Thanks for advice!

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