-
-
Save novel-yet-trivial/3eddfce704db3082e38c84664fc1fdf8 to your computer and use it in GitHub Desktop.
#!/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() |
Quite good. It absolutely worked for me. You should definitely use this code to answer this StackOverflow question:
https://stackoverflow.com/questions/16188420/tkinter-scrollbar-for-frame
Works like a charm.
OMG this is perfect! i just made some alterations to fit my code but this is perfect
how can I find all the children in the frame?(the scrollable one)
I used to do this in the normal frame to delete all widgets on the normal frame:
for widget in frame.winfo_children():
widget.destroy()
how can I do the same for this?
how can I find all the children in the frame?(the scrollable one)
I used to do this in the normal frame to delete all widgets on the normal frame:for widget in frame.winfo_children(): widget.destroy()how can I do the same for this?
@rrayes3110
Widgets are supposed to be placed in the "inner" frame,
so you should probably get the children of inner frame for your loop.
Thank you. This example helps me to make "Scrolled window" without tkinter.tix = ScrolledWindow.
Your example really works for me, think you very much!!
This has been a huge help.
How do I resize the inner frame to be the same width as the outer frame? I have tried assigning different widths in the VerticalScrolledFrame constructor and in self.inner, but the only way I have been able to get the inner frame width to match the outer has been to resize the outer one.
EDIT:
According to winfo_width() for the self.canvas, its window, and self.inner, I am able to resize them using a configure-type binding event on self.outer, but the widgets inside of self.inner do not resize along with everything else.
Hello,
I was struggling with verticall scrollbar and read many content (mostly on stackoverflow), to find satisfying solution. Yours was the best, and almost perfect. Almost, becasue when I was using pack method, inner frame wasn't fill entire space in canvas (even it was defined in pack method). I made few changed (every changed line are commented) and it works fine for my purpose. For now inner frame is always resized to canvas size, and you can specify in widgets inside it their behaviour (which wasn't possible before). I decided to publish my solution here because someone may looks for it as I was. I think @kozmik-moore struggles the same problem. Am I wrong?
However I'm not sure is it correct and optimized. I know that even small mistake in tkinter may cause huge slow in app speed. Thats why it would be good to read some comment from author of original code. Thanks in advance @novel-yet-trivial
p.s. I attached two example images - with wrapper frame of parameter fill set to Y and second to BOTH.
@King-of-Kings-980 thanks for tip about my mistake. I fix it right now
#!/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.canvas.addtag_all("all") # (added) for configuring width
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(0, 0, window=self.inner, anchor='nw') # changed - starts from (0, 0)
self.canvas.bind("<Configure>", self._on_frame_configure) # (changed) canvas bind instead of inner
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()
width = self.canvas.winfo_width() # (added) to resize inner frame
self.canvas.config(scrollregion = (0,0, x2, max(y2, height)))
self.canvas.itemconfigure("all", width=width) # (added) to resize inner frame
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="blue") # changed for debug ("light gray" -> "blue")
frame.pack(fill=tk.BOTH, expand=True) # fill window; changed to pack
# wrapper added to demonstrate pack case
wrapper = tk.Frame(frame, bg='green')
wrapper.pack(expand=tk.YES, fill=tk.BOTH, side=tk.TOP)
for i in range(30):
# row added
row = tk.Frame(wrapper)
row.pack(expand=tk.NO, fill=tk.BOTH, side=tk.TOP)
label = tk.Label(row, text="This is a label "+str(i))
label.pack(expand=tk.YES, fill=tk.BOTH, side=tk.LEFT) # grid changed to pack
text = tk.Entry(row, textvariable="text")
text.pack(expand=tk.YES, fill=tk.BOTH, side=tk.LEFT) # grid changed to pack
root.mainloop()
wrapper_frame_fill_Y.png
wrapper_frame_fill_BOTH.png
This was exactly what I needed. I put a link to the original code in the comments of my project.
@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.
@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!
I use your script for a project. If you want me to add something in the comment of my code just tell me.
CupChat