Skip to content

Instantly share code, notes, and snippets.

@codiac-killer
Last active October 30, 2019 21:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codiac-killer/a9123cb6d2be99b9728fe05ac41bcf07 to your computer and use it in GitHub Desktop.
Save codiac-killer/a9123cb6d2be99b9728fe05ac41bcf07 to your computer and use it in GitHub Desktop.
Frames with scroll bars that function properly, written in python with tkinter.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#---------------------------------------------------------------------------
#This gist is heavily based on a gist from novel-yet-trivial
#https://gist.github.com/novel-yet-trivial/3eddfce704db3082e38c84664fc1fdf8
#---------------------------------------------------------------------------
import tkinter as tk
class AutoScrollbar(tk.Scrollbar):
# a scrollbar that hides itself if it's not needed. only
# works if you use the grid geometry manager.
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
# grid_remove is currently missing from Tkinter!
self.tk.call("grid", "remove", self)
else:
self.grid()
tk.Scrollbar.set(self, lo, hi)
def pack(self, **kw):
raise tk.TclError("cannot use pack with this widget")
def place(self, **kw):
raise tk.TclError("cannot use place with this widget")
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.outer.rowconfigure(0, weight =1)
self.outer.columnconfigure(0, weight = 1)
self.vsb = AutoScrollbar(self.outer, orient=tk.VERTICAL)
self.vsb.grid(row = 0, column = 1, sticky = 'ns')
self.canvas = tk.Canvas(self.outer, highlightthickness=0)
self.canvas.grid(row = 0, column = 0, sticky = 'nsew')
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.inner_id = self.canvas.create_window(0, 0, window=self.inner, anchor='nw')
self.inner.bind("<Configure>", self._on_frame_configure)
self.canvas.bind('<Configure>', self._on_configure_canvas)
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_configure_canvas(self,event):
if self.inner.winfo_reqwidth() != self.canvas.winfo_width():
# update the inner frame's width to fill the canvas
self.canvas.itemconfigure(self.inner_id, width=self.canvas.winfo_width())
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" )
class HorizontallScrolledFrame:
"""
A horizontally 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.outer.rowconfigure(0, weight =1)
self.outer.columnconfigure(0, weight = 1)
self.hsb = AutoScrollbar(self.outer, orient=tk.HORIZONTAL)
self.hsb.grid(row = 1, column = 0, sticky = 'ew')
self.canvas = tk.Canvas(self.outer, highlightthickness=0)
self.canvas.grid(row = 0, column = 0, sticky = 'nsew')
self.canvas['xscrollcommand'] = self.hsb.set
self.hsb['command'] = self.canvas.xview
self.inner = tk.Frame(self.canvas, bg=bg)
# pack the inner Frame into the Canvas with the topleft corner
self.inner_id = self.canvas.create_window(0, 0, window=self.inner, anchor='nw')
self.inner.bind("<Configure>", self._on_frame_configure)
self.canvas.bind('<Configure>', self._on_configure_canvas)
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_configure_canvas(self,event):
if self.inner.winfo_reqheight() != self.canvas.winfo_height():
# update the inner frame's width to fill the canvas
self.canvas.itemconfigure(self.inner_id, height=self.canvas.winfo_height())
def _on_frame_configure(self, event=None):
x1, y1, x2, y2 = self.canvas.bbox("all")
width = self.canvas.winfo_width()
self.canvas.config(scrollregion = (0,0, max(x2, width), y2))
class TwoDScrolledFrame:
"""
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.outer.rowconfigure(0, weight =1)
self.outer.columnconfigure(0, weight = 1)
self.vsb = AutoScrollbar(self.outer, orient=tk.VERTICAL)
self.vsb.grid(row = 0, column = 1, sticky = 'ns')
self.hsb = AutoScrollbar(self.outer, orient=tk.HORIZONTAL)
self.hsb.grid(row = 1, column = 0, sticky = 'ew')
self.canvas = tk.Canvas(self.outer, highlightthickness=0)
self.canvas.grid(row = 0, column = 0, sticky = 'nsew')
self.canvas['yscrollcommand'] = self.vsb.set
self.canvas['xscrollcommand'] = self.hsb.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.hsb['command'] = self.canvas.xview
self.inner = tk.Frame(self.canvas, bg=bg)
# pack the inner Frame into the Canvas at the topleft corner
self.inner_id = self.canvas.create_window(0, 0, window=self.inner, anchor='nw')
self.canvas.bind('<Configure>', self._on_canvas_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_canvas_configure(self, event=None):
x1, y1, x2, y2 = self.canvas.bbox("all")
height = self.canvas.winfo_height()
width = self.canvas.winfo_width()
self.canvas.config(scrollregion = (0,0, max(x2,width), 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" )
# **** 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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment