Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
class ScrollViewWidget(tk.Frame):
"""A custom vertical scrollview.
A scrollview the hides the scrollbar if it's not required (the height of the child
widgets is less than the height) and allows the same pack/grid/place control a
regular frame has.
Parameters
----------
parent : `tkinter.Frame`
Other Parameters
----------------
scroll_side : {'left', 'right'}
Changes the side the scrollbar appears on (the default is 'right').
width : int
Sets the width of the scrollview; also sets the initial width and min width.
if `fill_xy` is `(True, False)`.
height: int
Sets the height of the scrollview; also sets the initial height and min height
if `fill_xy` is `(False, True)`.
align : tuple
Two values that represent the xcoordinate and ycoordinate alignment of the scrollview widgets
where (0, 0) is the upper left corner (default is (50, 50)).
bind_wheel : bool
The default is `True` which implies a mouse wheel event is binded to the scrollview, see Notes.
scroll_width : int
Sets the width of the scrollbar in pixels, default is 16.
fill_xy : tuple(bool, bool)
Indicates if the the scrollview should expand in the (x, y) directions,
default is `(True, True)`.
Example Usage
-------------
root = tk.Tk()
parent_frame = tk.Frame(root)
parent_frame.pack(fill='both', expand=True)
scroll_widget = ScrollViewWidget(parent_frame)
for i in range(0, 50):
label = tk.Label(scroll_widget, text=f'Label #{i}')
label.pack(fill='both')
scroll_widget.auto_scale_width()
root.mainloop()
Notes
-----
This class implements the scrollable widgets by using the frame inside a canvas inside a frame
approach. <Enter> and <Leave> events are binded to the canvas, and using a scrollview within
a scrollview is untested, so abnormal behavior might occur.
For examples see :class:`~.build_scrollview`.
Also, as of 10/15/2019, the scrollview is vertical only, so `fill_xy` in the y direction
only applies when the scrollbar is pack forgot, it only makes sense (and works) when
there is no scrollbar in the canvas region, so the window is expanded to fill the
space in the y direction. UPDATE: FILL Y NOT WORKING.
"""
def __init__(self, parent,
width: int = 100,
height: int = 200,
align: tuple = (50, 50),
scroll_side: str = 'right',
bind_wheel: bool = True,
scroll_width: int = 16,
fill_xy: tuple = (True, True),
*args,
**kwargs):
self._parent = parent
self._width = width
self._height = height
self._align = align
self._scroll_side = scroll_side
self._bind_wheel = bind_wheel
self._locked = False
self._scroll_width = scroll_width
self._fill_xy = fill_xy
self.curr_x_pos = align[0]
self.curr_y_pos = align[1]
self._init_canvas()
tk.Frame.__init__(self, self.canvas_, **kwargs)
self._window_id = self.canvas_.create_window(self._align[0], self._align[1], window=self, anchor='ne')
if self._bind_wheel:
self.canvas_.bind('<Enter>', self._bound_to_mousewheel)
self.canvas_.bind('<Leave>', self._unbound_to_mousewheel)
self.bind('<Configure>', self._on_frame_configure)
self.canvas_.bind('<Configure>', self._on_canvas_configure)
self.top()
def _init_canvas(self):
"""Creates the scroll GUI."""
if self._scroll_side == 'right':
canvas_side = 'left'
else:
canvas_side = 'right'
self.scrollbar_ = tk.Scrollbar(self._parent, width=self._scroll_width)
self.canvas_ = tk.Canvas(self._parent, borderwidth=0, highlightthickness=0)
self.scrollbar_.config(command=self.canvas_.yview)
self.canvas_.config(yscrollcommand=self.set_, width=self._width, height=self._height)
self.scrollbar_.pack(side=self._scroll_side, fill='y')
self.canvas_.pack(side=canvas_side, fill='both', expand=True)
def _on_frame_configure(self, event):
"""Updates scroll region to contain entire window."""
self.canvas_.configure(scrollregion=self.canvas_.bbox('all'))
def _on_canvas_configure(self, event):
"""Updates window region to fill entire canvas region."""
if self._fill_xy[0]: # fill x
self.canvas_.itemconfig(self._window_id, width=event.width)
#if self._fill_xy[1]: # fill y ... bugged
# self.canvas_.itemconfig(self._window_id, height=event.height)
# if self._locked:
# self.canvas_.itemconfig(self._window_id, width=self._width)
# else:
# self.canvas_.itemconfig(self._window_id, width=event.width)
def _bound_to_mousewheel(self, event=None):
"""Binds mousewheel to scrollview when cursor is over body frame."""
self.canvas_.bind_all('<MouseWheel>', self._on_mousewheel)
def _unbound_to_mousewheel(self, event=None):
"""Unbinds mousewheel to scrollview when cursor leaves body frame."""
self.canvas_.unbind_all('<MouseWheel>')
def _on_mousewheel(self, event):
"""Controlls scrollview from mousewheel."""
if not self._locked:
self.canvas_.yview_scroll(int(-1*(event.delta/120)), 'units')
def set_(self, lo, hi):
"""Auto hides the scrollbar if body is small."""
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.scrollbar_.pack_forget()
self.top()
self.lock()
else:
if self._locked:
self.unlock()
self.scrollbar_.pack(side=self._scroll_side, fill='y')
self.scrollbar_.set(lo, hi)
def move_window(self, x_offset, y_offset):
"""Moves the window by offset amount."""
self.canvas_.move(self._window_id, x_offset, y_offset)
def set_width(self, w):
"""Sets the width of the scrollview."""
self.canvas_['width'] = w
def set_height(self, h):
"""Sets the height of the scrollview."""
self.canvas_['height'] = h
def top(self):
"""Scrolls to the top y position."""
self.update_idletasks()
self.canvas_.yview_moveto(0.0)
def bottom(self):
"""Scrolls to the bottom y position."""
self.update_idletasks()
self.canvas_.yview_moveto(1.0)
def unlock(self):
"""Binds scrollbar to canvas."""
self._locked = False
def lock(self):
"""Unbinds scollbar to canvas."""
self._locked = True
def auto_scale_width(self):
"""Auto scales the width based on the first child widget."""
self.update_idletasks()
if self.winfo_children():
widget = self.winfo_children()[0]
width = round(widget.winfo_reqwidth(), -1) + 10
self.set_width(width)
else:
raise IndexError('Scrollview has no children to autoscale the width for.')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment