How do I add a scrollbar to my tkinter frame? [duplicate] - python
My objective is to add a vertical scroll bar to a frame which has several labels in it. The scroll bar should automatically enabled as soon as the labels inside the frame exceed the height of the frame. After searching through, I found this useful post. Based on that post I understand that in order to achieve what i want, (correct me if I am wrong, I am a beginner) I have to create a Frame first, then create a Canvas inside that frame and stick the scroll bar to that frame as well. After that, create another frame and put it inside the canvas as a window object. So, I finally come up with this:
from Tkinter import *
def data():
for i in range(50):
Label(frame,text=i).grid(row=i,column=0)
Label(frame,text="my text"+str(i)).grid(row=i,column=1)
Label(frame,text="..........").grid(row=i,column=2)
def myfunction(event):
canvas.configure(scrollregion=canvas.bbox("all"),width=200,height=200)
root=Tk()
sizex = 800
sizey = 600
posx = 100
posy = 100
root.wm_geometry("%dx%d+%d+%d" % (sizex, sizey, posx, posy))
myframe=Frame(root,relief=GROOVE,width=50,height=100,bd=1)
myframe.place(x=10,y=10)
canvas=Canvas(myframe)
frame=Frame(canvas)
myscrollbar=Scrollbar(myframe,orient="vertical",command=canvas.yview)
canvas.configure(yscrollcommand=myscrollbar.set)
myscrollbar.pack(side="right",fill="y")
canvas.pack(side="left")
canvas.create_window((0,0),window=frame,anchor='nw')
frame.bind("<Configure>",myfunction)
data()
root.mainloop()
Am I doing it right? Is there better/smarter way to achieve the output this code gave me?
Why must I use grid method? (I tried place method, but none of the labels appear on the canvas.)
What so special about using anchor='nw' when creating window on canvas?
Please keep your answer simple, as I am a beginner.
Here's example code adapted from the VerticalScrolledFrame page on the now defunct Tkinter Wiki that's been modified to run on Python 2.7 and 3+.
try: # Python 2
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.constants import *
except ImportError: # Python 2
import Tkinter as tk
import ttk
from tkinter.constants import *
# Based on
# https://web.archive.org/web/20170514022131id_/http://tkinter.unpythonic.net/wiki/VerticalScrolledFrame
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.
"""
def __init__(self, parent, *args, **kw):
ttk.Frame.__init__(self, parent, *args, **kw)
# Create a canvas object and a vertical scrollbar for scrolling it.
vscrollbar = ttk.Scrollbar(self, orient=VERTICAL)
vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
canvas = tk.Canvas(self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set)
canvas.pack(side=LEFT, fill=BOTH, expand=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=NW)
# 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())
interior.bind('<Configure>', _configure_interior)
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())
canvas.bind('<Configure>', _configure_canvas)
if __name__ == "__main__":
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
root = tk.Tk.__init__(self, *args, **kwargs)
self.frame = VerticalScrolledFrame(root)
self.frame.pack()
self.label = ttk.Label(self, text="Shrink the window to activate the scrollbar.")
self.label.pack()
buttons = []
for i in range(10):
buttons.append(ttk.Button(self.frame.interior, text="Button " + str(i)))
buttons[-1].pack()
app = SampleApp()
app.mainloop()
It does not yet have the mouse wheel bound to the scrollbar but it is possible. Scrolling with the wheel can get a bit bumpy, though.
edit:
to 1)
IMHO scrolling frames is somewhat tricky in Tkinter and does not seem to be done a lot. It seems there is no elegant way to do it.
One problem with your code is that you have to set the canvas size manually - that's what the example code I posted solves.
to 2)
You are talking about the data function? Place works for me, too. (In general I prefer grid).
to 3)
Well, it positions the window on the canvas.
One thing I noticed is that your example handles mouse wheel scrolling by default while the one I posted does not. Will have to look at that some time.
"Am i doing it right?Is there better/smarter way to achieve the output this code gave me?"
Generally speaking, yes, you're doing it right. Tkinter has no native scrollable container other than the canvas. As you can see, it's really not that difficult to set up. As your example shows, it only takes 5 or 6 lines of code to make it work -- depending on how you count lines.
"Why must i use grid method?(i tried place method, but none of the labels appear on the canvas?)"
You ask about why you must use grid. There is no requirement to use grid. Place, grid and pack can all be used. It's simply that some are more naturally suited to particular types of problems. In this case it looks like you're creating an actual grid -- rows and columns of labels -- so grid is the natural choice.
"What so special about using anchor='nw' when creating window on canvas?"
The anchor tells you what part of the window is positioned at the coordinates you give. By default, the center of the window will be placed at the coordinate. In the case of your code above, you want the upper left ("northwest") corner to be at the coordinate.
Please see my class that is a scrollable frame. It's vertical scrollbar is binded to <Mousewheel> event as well. So, all you have to do is to create a frame, fill it with widgets the way you like, and then make this frame a child of my ScrolledWindow.scrollwindow. Feel free to ask if something is unclear.
Used a lot from # Brayan Oakley answers to close to this questions
class ScrolledWindow(tk.Frame):
"""
1. Master widget gets scrollbars and a canvas. Scrollbars are connected
to canvas scrollregion.
2. self.scrollwindow is created and inserted into canvas
Usage Guideline:
Assign any widgets as children of <ScrolledWindow instance>.scrollwindow
to get them inserted into canvas
__init__(self, parent, canv_w = 400, canv_h = 400, *args, **kwargs)
docstring:
Parent = master of scrolled window
canv_w - width of canvas
canv_h - height of canvas
"""
def __init__(self, parent, canv_w = 400, canv_h = 400, *args, **kwargs):
"""Parent = master of scrolled window
canv_w - width of canvas
canv_h - height of canvas
"""
super().__init__(parent, *args, **kwargs)
self.parent = parent
# creating a scrollbars
self.xscrlbr = ttk.Scrollbar(self.parent, orient = 'horizontal')
self.xscrlbr.grid(column = 0, row = 1, sticky = 'ew', columnspan = 2)
self.yscrlbr = ttk.Scrollbar(self.parent)
self.yscrlbr.grid(column = 1, row = 0, sticky = 'ns')
# creating a canvas
self.canv = tk.Canvas(self.parent)
self.canv.config(relief = 'flat',
width = 10,
heigh = 10, bd = 2)
# placing a canvas into frame
self.canv.grid(column = 0, row = 0, sticky = 'nsew')
# accociating scrollbar comands to canvas scroling
self.xscrlbr.config(command = self.canv.xview)
self.yscrlbr.config(command = self.canv.yview)
# creating a frame to inserto to canvas
self.scrollwindow = ttk.Frame(self.parent)
self.canv.create_window(0, 0, window = self.scrollwindow, anchor = 'nw')
self.canv.config(xscrollcommand = self.xscrlbr.set,
yscrollcommand = self.yscrlbr.set,
scrollregion = (0, 0, 100, 100))
self.yscrlbr.lift(self.scrollwindow)
self.xscrlbr.lift(self.scrollwindow)
self.scrollwindow.bind('<Configure>', self._configure_window)
self.scrollwindow.bind('<Enter>', self._bound_to_mousewheel)
self.scrollwindow.bind('<Leave>', self._unbound_to_mousewheel)
return
def _bound_to_mousewheel(self, event):
self.canv.bind_all("<MouseWheel>", self._on_mousewheel)
def _unbound_to_mousewheel(self, event):
self.canv.unbind_all("<MouseWheel>")
def _on_mousewheel(self, event):
self.canv.yview_scroll(int(-1*(event.delta/120)), "units")
def _configure_window(self, event):
# update the scrollbars to match the size of the inner frame
size = (self.scrollwindow.winfo_reqwidth(), self.scrollwindow.winfo_reqheight())
self.canv.config(scrollregion='0 0 %s %s' % size)
if self.scrollwindow.winfo_reqwidth() != self.canv.winfo_width():
# update the canvas's width to fit the inner frame
self.canv.config(width = self.scrollwindow.winfo_reqwidth())
if self.scrollwindow.winfo_reqheight() != self.canv.winfo_height():
# update the canvas's width to fit the inner frame
self.canv.config(height = self.scrollwindow.winfo_reqheight())
For anyone who stumbles across this (as it did when looking for my own gist) I maintain a gist for exactly this purpose at https://gist.github.com/mp035/9f2027c3ef9172264532fcd6262f3b01 It has scrollwheel support for various operating systems, is commented, and has a built-in demo in the file.
We can add scroll bar even without using Canvas. I have read it in many other post we can't add vertical scroll bar in frame directly etc etc. But after doing many experiment found out way to add vertical as well as horizontal scroll bar :). Please find below code which is used to create scroll bar in treeView and frame.
f = Tkinter.Frame(self.master,width=3)
f.grid(row=2, column=0, columnspan=8, rowspan=10, pady=30, padx=30)
f.config(width=5)
self.tree = ttk.Treeview(f, selectmode="extended")
scbHDirSel =tk.Scrollbar(f, orient=Tkinter.HORIZONTAL, command=self.tree.xview)
scbVDirSel =tk.Scrollbar(f, orient=Tkinter.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scbVDirSel.set, xscrollcommand=scbHDirSel.set)
self.tree["columns"] = (self.columnListOutput)
self.tree.column("#0", width=40)
self.tree.heading("#0", text='SrNo', anchor='w')
self.tree.grid(row=2, column=0, sticky=Tkinter.NSEW,in_=f, columnspan=10, rowspan=10)
scbVDirSel.grid(row=2, column=10, rowspan=10, sticky=Tkinter.NS, in_=f)
scbHDirSel.grid(row=14, column=0, rowspan=2, sticky=Tkinter.EW,in_=f)
f.rowconfigure(0, weight=1)
f.columnconfigure(0, weight=1)
It is nessesery to configure Scrollbar in case of using with Canvas
by sending to Canvas xscrollcommand attribute Scrollbar.set method and
to Scrollbar command attribute Canvas.yview (xview) method.
Canvas.yview method after scrollbar was moved recieve *args in next formatting:
tuple('move_to', '<some_absolute_float_value_of_top_of_scrollbar_region>')
In case of implementing scrollability to widget,
Recieving region and translating scrollbar_region (whith element viewable and whith not) features must be created.
Region is `tuple(float, float)' representing open to see part of all elements.
Not ideal bechavior showed in this solution (without using tk.Canvas)
import tkinter as tk
from tkinter import ttk
class ItemizeFrame(ttk.Frame, list):
def __init__(self,
*args,
scroll_upd_callback = lambda x: x,
visible_els: int = 10,
**kwargs):
list.__init__(self)
ttk.Frame.__init__(self, *args, **kwargs)
ttk.Style().configure('Small.TButton', background='red', width=2, height=2, padx=3, pady=3)
ttk.Style().configure('Sep.TFrame', padx=3, pady=3)
self.scroll_upd_callback = scroll_upd_callback
self.visible_els = visible_els
self.visible_st_idx = 0
self.pseudo_scroll_element_cursor_line = 0.5*1/visible_els
def append(self, item: ttk.Widget, **kw):
e = item(self, **kw)
super().append(e)
e.pack(fill='x')
self._update_visible_els()
def _update_visable_id_callback(self):
for id_, entry_ in enumerate(self):
entry_.set_id(id_)
def pop(self, index=None):
e = super().pop(index)
e.destroy()
self._update_visible_els()
def __getitem__(self, idx) -> ttk.Widget:
return list.__getitem__(self, idx)
# indicators computing and application
#property
def visible_end_idx(self):
return self.visible_st_idx + self.visible_els -1
#property
def visible_area_ratio(self) -> tuple[float, float]:
total = len(self)
st_val = 0.0
end_val = 1.0
if total > self.visible_els:
end_val = 1.0 - (total-self.visible_end_idx)/total
st_val = self.visible_st_idx / total
st_val = st_val + self.pseudo_scroll_element_cursor_line
end_val = end_val + self.pseudo_scroll_element_cursor_line
return (st_val, end_val)
def _update_scroll_widget(self):
self.scroll_upd_callback(*self.visible_area_ratio)
def set_yview(self, move_to_ratio):
base_pseudo_ratio = 0.5*1/self.visible_els
total = len(self)
max_ratio = (total - self.visible_els)/total+base_pseudo_ratio
if move_to_ratio < 0:
possible_st_el_pseudo_part = base_pseudo_ratio
possible_st_el_idx = 0
if max_ratio < move_to_ratio:
possible_st_el_idx = total - self.visible_els
possible_st_el_pseudo_part = base_pseudo_ratio
else :
el_idx_raw = move_to_ratio * total
el_idx_round = round(el_idx_raw)
el_idx_pseudo = (el_idx_raw - el_idx_round)*1/self.visible_els
possible_st_el_idx = el_idx_round
possible_st_el_pseudo_part = el_idx_pseudo
self.visible_st_idx = possible_st_el_idx
self.pseudo_scroll_element_cursor_line = possible_st_el_pseudo_part
self._update_visible_els()
def _update_visible_els(self):
for el in self:
el.pack_forget()
for num, el in enumerate(self):
if self.visible_st_idx <= num and num <= self.visible_end_idx:
el.pack()
self._update_scroll_widget()
class ScrollableFrame(ttk.Frame):
def __init__(self, *args, **kwargs):
kw = dict(width=400, height=300)
kw.update(kwargs)
super().__init__(*args, **kw)
self.scroll = ttk.Scrollbar(self, command=self.on_scroll)
self.scroll.pack(expand=True, fill='y', side='right')
self.view = ItemizeFrame(
self,
scroll_upd_callback=self.scroll.set,
**kwargs
)
self.view.pack(expand=True, fill='both')#, side='left')
def on_scroll(self, *args, **kwargs):
value_raw = float(args[1])
self.view.set_yview(value_raw)
Usecase
class App(tk.Tk):
def __init__(self):
super().__init__()
self.frame = ScrollableFrame(self)
self.frame.pack()
def test_fill(self):
for i in range(15):
self.frame.view.append(ttk.Entry)
class Test:
#staticmethod
def v2():
app = App()
app.test_fill()
app.mainloop()
Test.v2()
After I watching many answers, I got it:
import tkinter as tk
root = tk.Tk()
root.title("音樂編輯器")
root.geometry("600x480")
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
'''When window size change, canvas size will change,
use this line to change its item size (width).'''
canvas.itemconfigure(wrapFrame, width=canvas.winfo_width())
canvas = tk.Canvas(root, highlightthickness=0)
frame = tk.Frame(canvas, background="#FFFFFF")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(fill="both", expand=1, anchor="nw") #canvas size is relative to window size.
wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw")
# When the window size change, it will call this function
canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25))
L1.pack(anchor="n")
for i in range(100):
input = tk.Entry(frame)
input.pack()
root.mainloop()
Specifies the size of the scrollable frame by changing canvas and scrollbar position and size.
import tkinter as tk
root = tk.Tk()
root.title("音樂編輯器")
root.geometry("600x480")
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
canvas.itemconfigure(wrapFrame, width=canvas.winfo_width())
canvas = tk.Canvas(root, highlightthickness=0)
frame = tk.Frame(canvas, background="#FFFFFF")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.place(relx=0.9, y=0, relwidth=0.1, relheight=0.5)
canvas.place(x=0, y=0, relwidth=0.9, relheight=0.5)
wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw")
canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25))
L1.pack(anchor="n")
for i in range(100):
input = tk.Entry(frame)
input.pack()
root.mainloop()
Specifies the size of the scrollable frame by writing them to outerFrame.
import tkinter as tk
root = tk.Tk()
root.title("音樂編輯器")
root.geometry("600x480")
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
canvas.itemconfigure(wrapFrame, width=canvas.winfo_width())
outerFrame = tk.Frame(root)
canvas = tk.Canvas(outerFrame, highlightthickness=0)
frame = tk.Frame(canvas, background="#FFFFFF")
vsb = tk.Scrollbar(outerFrame, orient="vertical", command=canvas.yview)
canvas.config(yscrollcommand=vsb.set)
outerFrame.place(relx=0.25, rely=0.1, relwidth=0.5, relheight=0.5)
vsb.pack(side="right", fill="y")
canvas.pack(fill="both", expand=1, anchor="nw")
wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw")
canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25))
L1.pack(anchor="n")
for i in range(100):
input = tk.Entry(frame)
input.pack()
root.mainloop()
The items inner the frame can use pack or grid (only choose one), but place cannot be used alone. If you want to use place, you need to expand the layout(height) with pack or grid first.
import tkinter as tk
root = tk.Tk()
root.title("音樂編輯器")
root.geometry("600x480")
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
canvas.itemconfigure(wrapFrame, width=canvas.winfo_width())
canvas = tk.Canvas(root, highlightthickness=0)
frame = tk.Frame(canvas, background="#FFFFFF")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(fill="both", expand=1, anchor="nw")
wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw")
canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25))
L1.pack(anchor="n")
for i in range(100):
input = tk.Entry(frame)
input.pack()
L1 = tk.Label(frame, text="我是Label")
L1.place(x=0, rely=0.5)
root.mainloop()
Use mouse wheel:
tkinter: binding mousewheel to scrollbar
import tkinter as tk
root = tk.Tk()
root.title("音樂編輯器")
root.geometry("600x480")
def onFrameConfigure(canvas):
canvas.configure(scrollregion=canvas.bbox("all"))
canvas.itemconfigure(wrapFrame, width=canvas.winfo_width())
def on_mouse_wheel(event, scale=3):
#only care event.delta is - or +, scroll down or up
if event.delta<0:
canvas.yview_scroll(scale, "units")
else:
canvas.yview_scroll(-scale, "units")
canvas = tk.Canvas(root, highlightthickness=0)
frame = tk.Frame(canvas, background="#FFFFFF")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(fill="both", expand=1, anchor="nw")
wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw")
canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
canvas.bind("<Enter>", lambda event: canvas.bind_all("<MouseWheel>", on_mouse_wheel)) # on mouse enter
canvas.bind("<Leave>", lambda event: canvas.unbind_all("<MouseWheel>")) # on mouse leave
L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25))
L1.pack(anchor="n")
for i in range(100):
input = tk.Entry(frame)
input.pack()
root.mainloop()
Export to class:
import tkinter as tk
root = tk.Tk()
root.title("音樂編輯器")
root.geometry("600x480")
class scrollFrame():
def __init__(self, **options):
outerFrame = tk.Frame(root)
canvas = tk.Canvas(outerFrame, highlightthickness=0)
vsb = tk.Scrollbar(outerFrame, orient="vertical", command=canvas.yview)
vsb.pack(side="right", fill="y")
canvas.pack(fill="both", expand=1, anchor="nw")
frame = tk.Frame(canvas, **options)
wrapFrameId = canvas.create_window((0,0), window=frame, anchor="nw")
canvas.config(yscrollcommand=vsb.set)
canvas.bind("<Configure>", lambda event: self.onFrameConfigure())
canvas.bind("<Enter>", lambda event: canvas.bind_all("<MouseWheel>", self.on_mouse_wheel)) # on mouse enter
canvas.bind("<Leave>", lambda event: canvas.unbind_all("<MouseWheel>")) # on mouse leave
self.outerFrame, self.canvas, self.vsb, self.frame, self.wrapFrameId = outerFrame, canvas, vsb, frame, wrapFrameId
def onFrameConfigure(self):
canvas = self.canvas
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
canvas.itemconfigure(self.wrapFrameId, width=canvas.winfo_width())
def on_mouse_wheel(self, event, scale=3):
canvas = self.canvas
#only care event.delta is - or +, scroll down or up
if event.delta<0:
canvas.yview_scroll(scale, "units")
else:
canvas.yview_scroll(-scale, "units")
frame = scrollFrame(background="#FFFFFF")
frame.outerFrame.place(relx=0.15, rely=0.1, relwidth=0.7, relheight=0.8)
L1 = tk.Label(frame.frame, text="音樂編輯器", bg="#556644", font=("",25))
L1.pack(anchor="n")
for i in range(100):
input = tk.Entry(frame.frame)
input.pack()
root.mainloop()
According:
https://stackoverflow.com/a/3092341/19470749
https://stackoverflow.com/a/16198198/19470749
https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/create_window.html
Not 100% sure if this solution is on topic (since it explicitely asks for a scrollable FRAME), but the text widget is basically a scrollable Frame.
From documentation of the Text widget:
"Like canvas widgets, text widgets can contain images and any other Tk widgets (including frames containing many other widgets). In a sense, this allows the text widget to work as a geometry manager in its own right. "
Text widgets are very easy to use, and can be made scrollable. So instead of using a special Class like the Scrollable Frame, I think the Text widget is a great option.
Below my code, for a basic example of a scrollable text widget holding 100 buttons:
from tkinter import Tk, Button, Text,Scrollbar
class test:
def __init__(self):
self.win = Tk()
text = Text(self.win, width=40, height=10, wrap = "none")
ys = Scrollbar(self.win, orient = 'vertical', command = text.yview)
text['yscrollcommand'] = ys.set
text.grid(column = 0, row = 0, sticky = 'nwes')
ys.grid(column = 1, row = 0, sticky = 'ns')
for x in range(1,100):
b = Button(text, text='Push Me')
text.window_create("end", window=b)
text.insert("end",'\n')
self.win.mainloop()
test = test()
This is at least the method I am going to use for my scrollable frames. Not sure if there is a better solution then the newline insertion to make the widgets organised vertically. But it works.
Related
How can I change the size of the buttons inside a frame with create_window?
my problem is that I can't change the size of the buttons so that they fill the frame inside the "canvas". The buttons stay at a very small size, they only resize with the text that is entered or I could use width but I want to fill as if it were relwidth so I can resize easily I've tried researching, but research just doesn't work. I put fill="x" in set_news to fill the width but it can't. from tkinter import Frame, Label, Button, Entry, messagebox, ttk, Scrollbar, Canvas import base64 import json class WatchNewsFrame(Frame): name = "WatchNewsFrame" def __init__(self, parent, file): super().__init__() self.Parent = parent self.file = file self.initializecomponents() def set_news(self): data = json.load(open(self.file))["News"] for key in range(30): Panel = Button(self.frame, height=10) Panel.config(bg="#656565", activebackground="#808080") Panel.pack(fill="x") pass def initializecomponents(self): Frame.__init__(self, self.Parent) self.canvas = Canvas(self) self.Scrollbar = ttk.Scrollbar(self, orient="vertical") self.frame = Frame(self.canvas) # this frame self.config(background=self.Parent["background"]) self.place(relwidth=0.95, relheight=0.95, relx=0.5, rely=0.5, anchor="center") # ItemsScrooll self.Scrollbar.config(orient="vertical", command=self.canvas.yview) self.Scrollbar.place(relheight=.85, relwidth=.015, relx=.95, rely=.5, anchor="center") # itemsFrame self.canvas.config(bg="#606060", highlightbackground="#FFFFFF", yscrollcommand=self.Scrollbar.set) self.canvas.create_window(2, 0, window=self.frame, anchor="nw",) self.canvas.place(relheight=0.85, relwidth=0.9, relx=0.5, rely=0.5, anchor="center") self.canvas.bind('<Configure>', lambda e: self.canvas.config(scrollregion=self.canvas.bbox("all"))) # events self.set_news() pass pass
Since you have used fill="x" on those buttons, you just need to set the width of the frame the same as the canvas when the canvas is resized, then the width of those buttons will be changed as well. ... # save the item ID of the frame self.frame_id = self.canvas.create_window(2, 0, window=self.frame, anchor="nw") ... # set width of the frame same as the canvas self.canvas.bind('<Configure>', lambda e: self.canvas.itemconfigure(self.frame_id, width=e.width)) # update scrollregion when the frame is resized self.frame.bind('<Configure>', lambda e: self.canvas.config(scrollregion=self.canvas.bbox("all"))) ...
Trying to change a scrolled canvas width with mouse wheel
I'm trying to control multiple canvases widths with the mouse wheel. What I have so far is this: import tkinter as tk class App(tk.Frame): row_amount = 3 def __init__(self, root): super(App, self).__init__(root) self.root = root self.main_frame = tk.Frame(root) self.main_frame.pack(expand=True, fill=tk.BOTH) self.row_collection = RowCollection(root, self.main_frame) for i in range(App.row_amount): self.row_collection.row() window_height = App.row_amount * 100 window_width = root.winfo_screenwidth() - 30 root.geometry(f'{window_width}x{window_height}+0+0') self.row_collection.right_frame.grid_columnconfigure(0, weight=1) self.row_collection.left_frame.grid_columnconfigure(0, weight=1) self.pack() class RowCollection: """Collection of rows""" def __init__(self, root, frame): self.row_list = [] self.root = root self.frame = frame self.right_frame = tk.Frame(self.frame, bg='red') self.right_frame.pack(side=tk.RIGHT, expand=tk.YES, fill=tk.BOTH) self.left_frame = tk.Frame(self.frame) self.left_frame.pack(side=tk.LEFT, fill=tk.Y) self.scrollbar = tk.Scrollbar(self.right_frame, orient=tk.HORIZONTAL) self.scrollbar.config(command=self.scroll_x) def row(self): row = Row(self) self.row_list.append(row) return row def scroll_x(self, *args): for row in self.row_list: row.canvas.xview(*args) def zoomer(self, event=None): print('zooming') for row in self.row_list: scale_factor = 0.1 curr_width = row.canvas.winfo_reqwidth() print(f'event delta={event.delta}') if event.delta > 0: row.canvas.config(width=curr_width * (1 + scale_factor)) elif event.delta < 0: row.canvas.config(width=curr_width * (1 - scale_factor)) row.canvas.configure(scrollregion=row.canvas.bbox('all')) class Row: """Every row consists of a label on the left side and a canvas with a line on the right side""" row_count = 0 label_width = 15 line_weight = 3 line_yoffset = 3 padx = 20 def __init__(self, collection): self.frame = collection.frame self.root = collection.root self.collection = collection self.canvas = None self.label = None self.text = f'Canvas {Row.row_count}' self.height = 100 self.root.update() self.label = tk.Label(self.collection.left_frame, text=self.text, height=1, width=Row.label_width, relief='raised') self.label.grid(row=Row.row_count, column=0, sticky='ns') # configure row size to match future canvas height self.collection.left_frame.grid_rowconfigure(Row.row_count, minsize=self.height) self.root.update() self.canvas = tk.Canvas(self.collection.right_frame, width=10000, height=self.height, bg='white', highlightthickness=0) self.canvas.grid(row=Row.row_count, column=0, sticky=tk.W) self.root.update() # draw line self.line = self.canvas.create_rectangle(self.padx, self.canvas.winfo_height() - Row.line_yoffset, self.canvas.winfo_width() - self.padx, self.canvas.winfo_height() - Row.line_yoffset + Row.line_weight, fill='#000000', width=0, tags='line') # config canvas self.canvas.config(scrollregion=self.canvas.bbox('all')) self.canvas.config(xscrollcommand=self.collection.scrollbar.set) self.canvas.bind('<Configure>', lambda event: self.canvas.configure(scrollregion=self.canvas.bbox('all'))) self.canvas.bind('<MouseWheel>', self.collection.zoomer) # Create point at canvas edge to prevent scrolling from removing padding self.bounding_point = self.canvas.create_rectangle(0, 0, 0, 0, width=0) self.bounding_point = self.canvas.create_rectangle(self.canvas.winfo_width(), self.canvas.winfo_width(), self.canvas.winfo_width(), self.canvas.winfo_width(), width=0) Row.row_count += 1 self.collection.scrollbar.grid(row=Row.row_count, column=0, sticky='ew') if __name__ == '__main__': root = tk.Tk() app = App(root) root.mainloop() The canvases themselves are inside right_frame, and the number of canvases is given by row_amount. The left_frame contains labels for each of the canvases. The canvases should be allowed to be pretty wide, so I initially set a width value of 10000. Because of that, they start partially visible, with the rest being accessible via a scrollbar. What I would like is for the mouse wheel to control the size of the canvas as a whole (that is, both what is currently visible and what could be viewed using the scrollbar), similar to what would happen in an audio or video editing software timeline. Right now, when I use the mouse wheel, what seems to get resized is not the whole canvas, but only the 'visible' portion. Resize it to be small enough and you can start to see it's frame background on the right portion of the window. What am I missing here?
What am I missing here? I think what you're missing is that the drawable area of the canvas is not at all related to the physical size of the canvas widget. You do not need to resize the canvas once it has been created. You can draw well past the borders of the widget. If you want to be able to scroll elements into view that are not part of the visible canvas, you must configure the scrollregion to define the area of the virtual canvas that should be visible. You said in a comment you're trying to create a timeline. Here's an example of a canvas widget that "grows" by adding a tickmark every second. Notice that the canvas is only 500,100, but the drawable area gets extended every second. import tkinter as tk root = tk.Tk() canvas = tk.Canvas(root, width=500, height=100, bg="black") vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview) hsb = tk.Scrollbar(root, orient="horizontal", command=canvas.xview) canvas.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) canvas.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") root.grid_rowconfigure(0, weight=1) root.grid_columnconfigure(0, weight=1) counter = 0 def add_tick(): global counter # get the current state of the scrollbar. We'll use this later # to determine if we should auto-scroll xview = canvas.xview() # draw a new tickmark counter += 1 x = counter * 50 canvas.create_text(x, 52, anchor="n", text=counter, fill="white") canvas.create_line(x, 40, x, 50, width=3, fill="red") # update the scrollable region to include the new tickmark canvas.configure(scrollregion=canvas.bbox("all")) # autoscroll, only if the widget was already scrolled # as far to the right as possible if int(xview[1]) == 1: canvas.xview_moveto(1.0) canvas.after(1000, add_tick) add_tick() root.mainloop()
How to resize scrollable frame when the window is expanded or maximized?
When the window is expanded or maximized, the frame is just constant. The size is constant. I want the over all frame to move as I expand or maximize the window. How can this be done? from tkinter import * def data(): for i in range(1000): if (i % 2) == 0: l4 = Label(frame, text="Size of rectangle:") l4.grid(row=i, column=0) en = Entry(frame) en.grid(row=i, column=1) b3 = Button(frame, text="Save") b3.grid(row=1001, column=0) b4 = Button(frame, text="Back") b4.grid(row=1001, column=1) def myfunction(event): canvas.configure(scrollregion=canvas.bbox("all"),width=250,height=700) def _on_mousewheel(event): canvas.yview_scroll(-1*(event.delta/120), "units") root=Tk() sizex = 272 sizey = 707 posx = 100 posy = 100 root.wm_geometry("%dx%d+%d+%d" % (sizex, sizey, posx, posy)) myframe=Frame(root) myframe.place(x=0,y=0) canvas=Canvas(myframe) frame=Frame(canvas) myscrollbar=Scrollbar(myframe,orient="vertical",command=canvas.yview) canvas.configure(yscrollcommand=myscrollbar.set) canvas.bind_all('<MouseWheel>', lambda event: canvas.yview_scroll(int(-1*(event.delta/120)), "units")) myscrollbar.pack(side="right",fill="y") canvas.pack(side="left") canvas.create_window((0,0),window=frame,anchor='nw') frame.bind("<Configure>",myfunction) data() root.bind("<MouseWheel>", myfunction) root.mainloop()
The canvas will generate a <Configure> event when it is resized. You can bind to this event and reset the size of the inner frame to match the width of the canvas when this happens. However, in your specific case you've got a series of stacked widgets, none of which will grow when you resize the window. Because of that, even when you resize the window, the canvas doesn't grow. Because the canvas doesn't grow, the event won'g fire. You'll need to use the appropriate options to the geometry managers to make sure the entire hierarchy of windows grows and shrinks appropriately, and then add the binding to set the size of the inner window. Here's a very basic example. It starts by creating a canvas and packing it so that it fills the root window. It then adds a frame inside the canvas, and arranges for the width of the frame to change whenever the canvas is resized. import tkinter as tk def handle_canvas_resize(event): canvas.itemconfigure(window_id, width=event.width) def handle_frame_resize(event): canvas.configure(scrollregion=canvas.bbox("all")) root = tk.Tk() canvas = tk.Canvas(root, width=200, height=200) scrollbar = tk.Scrollbar(root, command=canvas.yview) canvas.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") canvas.pack(fill="both", expand=True) inner_frame = tk.Frame(canvas, background="bisque") window_id = canvas.create_window(2,2, window=inner_frame) for i in range(1, 101): label = tk.Label(inner_frame, text="label #{}".format(i)) label.pack(side="top", fill="x", padx=1, pady=1) inner_frame.bind("<Configure>", handle_frame_resize) canvas.bind("<Configure>", handle_canvas_resize) root.after_idle(canvas.yview_moveto, 0) root.mainloop()
Why does the tkinter scrollbar on a canvas with inner frame get disabled on declaring pack_propagate(0)?
My code has a Frame, a Canvas inside the Frame, and an inner Frame inside the Canvas. I want to put Entry boxes inside the inner Frame and fit them to the inner Frame via pack_propagate(0) to avoid pixel/font width conversions with the Entry widget's width option. However, this breaks the inner Frame's scroll functionality. I want to add Entry widgets dynamically above and below the first and last Entry widgets in the inner Frame, which I am currently doing using pack(before=). So I would like to stick with the packer if possible. How can I get the scrollbar working again? The following minimum working example has frame.pack_propagate(0) commented out, so the Entry widgets are not sized to the column correctly: import tkinter as tk import tkinter.ttk as ttk root = tk.Tk() myframe = ttk.Frame(root) myframe.pack() sb = ttk.Scrollbar(myframe) sb.pack(side=tk.RIGHT, fill=tk.Y, expand=1) canvas = tk.Canvas(myframe, width=200, height=300, scrollregion=(0, 0, 200, 300), yscrollcommand=sb.set) canvas.pack() frame = ttk.Frame(canvas, width=200, height=300) # inner Frame canvas.create_window((0, 0), window=frame, anchor='nw') sb.config(command=canvas.yview) frame.bind("<Configure>", lambda event: canvas.configure( scrollregion=canvas.bbox(tk.ALL))) # frame.pack_propagate(0) # How to enable this and ensure scrollbar works? s = tk.StringVar() s.set("I'm a box") for _ in range(100): eb = ttk.Entry(frame, textvariable=s) eb.pack() root.mainloop() Edit1: Added StringVar() and changed eb.grid() to eb.pack()
See example which resize inner Frame to Canvas - it uses self._canvas.bind('<Configure>', self.inner_resize) and inside method inner_resize() self._canvas.itemconfig(self._window, width=event.width) Full example import tkinter as tk import tkinter.ttk as ttk class ScrolledFrame(tk.Frame): def __init__(self, parent, vertical=True, horizontal=False): super().__init__(parent) # canvas for inner frame self._canvas = tk.Canvas(self, bg='red') self._canvas.grid(row=0, column=0, sticky='news') # changed # create right scrollbar and connect to canvas Y self._vertical_bar = tk.Scrollbar(self, orient='vertical', command=self._canvas.yview) if vertical: self._vertical_bar.grid(row=0, column=1, sticky='ns') self._canvas.configure(yscrollcommand=self._vertical_bar.set) # create bottom scrollbar and connect to canvas X self._horizontal_bar = tk.Scrollbar(self, orient='horizontal', command=self._canvas.xview) if horizontal: self._horizontal_bar.grid(row=1, column=0, sticky='we') self._canvas.configure(xscrollcommand=self._horizontal_bar.set) # inner frame for widgets self.inner = tk.Frame(self._canvas) self._window = self._canvas.create_window((0, 0), window=self.inner, anchor='nw') # autoresize inner frame self.columnconfigure(0, weight=1) # changed self.rowconfigure(0, weight=1) # changed # resize when configure changed self.inner.bind('<Configure>', self.resize) # resize inner frame to canvas size self.resize_width = False self.resize_height = False self._canvas.bind('<Configure>', self.inner_resize) def resize(self, event=None): self._canvas.configure(scrollregion=self._canvas.bbox('all')) def inner_resize(self, event): # resize inner frame to canvas size if self.resize_width: self._canvas.itemconfig(self._window, width=event.width) if self.resize_height: self._canvas.itemconfig(self._window, height=event.height) # --- main ---- root = tk.Tk() sf = ScrolledFrame(root) sf.resize_width = True # it will resize frame to canvas sf.pack(fill='both', expand=True) s = tk.StringVar() s.set("I'm a box") for _ in range(100): eb = ttk.Entry(sf.inner, textvariable=s) eb.pack(fill='x', expand=True) root.mainloop()
Add a scrollbar to tkinter - python 3 [duplicate]
My objective is to add a vertical scroll bar to a frame which has several labels in it. The scroll bar should automatically enabled as soon as the labels inside the frame exceed the height of the frame. After searching through, I found this useful post. Based on that post I understand that in order to achieve what i want, (correct me if I am wrong, I am a beginner) I have to create a Frame first, then create a Canvas inside that frame and stick the scroll bar to that frame as well. After that, create another frame and put it inside the canvas as a window object. So, I finally come up with this: from Tkinter import * def data(): for i in range(50): Label(frame,text=i).grid(row=i,column=0) Label(frame,text="my text"+str(i)).grid(row=i,column=1) Label(frame,text="..........").grid(row=i,column=2) def myfunction(event): canvas.configure(scrollregion=canvas.bbox("all"),width=200,height=200) root=Tk() sizex = 800 sizey = 600 posx = 100 posy = 100 root.wm_geometry("%dx%d+%d+%d" % (sizex, sizey, posx, posy)) myframe=Frame(root,relief=GROOVE,width=50,height=100,bd=1) myframe.place(x=10,y=10) canvas=Canvas(myframe) frame=Frame(canvas) myscrollbar=Scrollbar(myframe,orient="vertical",command=canvas.yview) canvas.configure(yscrollcommand=myscrollbar.set) myscrollbar.pack(side="right",fill="y") canvas.pack(side="left") canvas.create_window((0,0),window=frame,anchor='nw') frame.bind("<Configure>",myfunction) data() root.mainloop() Am I doing it right? Is there better/smarter way to achieve the output this code gave me? Why must I use grid method? (I tried place method, but none of the labels appear on the canvas.) What so special about using anchor='nw' when creating window on canvas? Please keep your answer simple, as I am a beginner.
Here's example code adapted from the VerticalScrolledFrame page on the now defunct Tkinter Wiki that's been modified to run on Python 2.7 and 3+. try: # Python 2 import tkinter as tk import tkinter.ttk as ttk from tkinter.constants import * except ImportError: # Python 2 import Tkinter as tk import ttk from tkinter.constants import * # Based on # https://web.archive.org/web/20170514022131id_/http://tkinter.unpythonic.net/wiki/VerticalScrolledFrame 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. """ def __init__(self, parent, *args, **kw): ttk.Frame.__init__(self, parent, *args, **kw) # Create a canvas object and a vertical scrollbar for scrolling it. vscrollbar = ttk.Scrollbar(self, orient=VERTICAL) vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) canvas = tk.Canvas(self, bd=0, highlightthickness=0, yscrollcommand=vscrollbar.set) canvas.pack(side=LEFT, fill=BOTH, expand=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=NW) # 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()) interior.bind('<Configure>', _configure_interior) 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()) canvas.bind('<Configure>', _configure_canvas) if __name__ == "__main__": class SampleApp(tk.Tk): def __init__(self, *args, **kwargs): root = tk.Tk.__init__(self, *args, **kwargs) self.frame = VerticalScrolledFrame(root) self.frame.pack() self.label = ttk.Label(self, text="Shrink the window to activate the scrollbar.") self.label.pack() buttons = [] for i in range(10): buttons.append(ttk.Button(self.frame.interior, text="Button " + str(i))) buttons[-1].pack() app = SampleApp() app.mainloop() It does not yet have the mouse wheel bound to the scrollbar but it is possible. Scrolling with the wheel can get a bit bumpy, though. edit: to 1) IMHO scrolling frames is somewhat tricky in Tkinter and does not seem to be done a lot. It seems there is no elegant way to do it. One problem with your code is that you have to set the canvas size manually - that's what the example code I posted solves. to 2) You are talking about the data function? Place works for me, too. (In general I prefer grid). to 3) Well, it positions the window on the canvas. One thing I noticed is that your example handles mouse wheel scrolling by default while the one I posted does not. Will have to look at that some time.
"Am i doing it right?Is there better/smarter way to achieve the output this code gave me?" Generally speaking, yes, you're doing it right. Tkinter has no native scrollable container other than the canvas. As you can see, it's really not that difficult to set up. As your example shows, it only takes 5 or 6 lines of code to make it work -- depending on how you count lines. "Why must i use grid method?(i tried place method, but none of the labels appear on the canvas?)" You ask about why you must use grid. There is no requirement to use grid. Place, grid and pack can all be used. It's simply that some are more naturally suited to particular types of problems. In this case it looks like you're creating an actual grid -- rows and columns of labels -- so grid is the natural choice. "What so special about using anchor='nw' when creating window on canvas?" The anchor tells you what part of the window is positioned at the coordinates you give. By default, the center of the window will be placed at the coordinate. In the case of your code above, you want the upper left ("northwest") corner to be at the coordinate.
Please see my class that is a scrollable frame. It's vertical scrollbar is binded to <Mousewheel> event as well. So, all you have to do is to create a frame, fill it with widgets the way you like, and then make this frame a child of my ScrolledWindow.scrollwindow. Feel free to ask if something is unclear. Used a lot from # Brayan Oakley answers to close to this questions class ScrolledWindow(tk.Frame): """ 1. Master widget gets scrollbars and a canvas. Scrollbars are connected to canvas scrollregion. 2. self.scrollwindow is created and inserted into canvas Usage Guideline: Assign any widgets as children of <ScrolledWindow instance>.scrollwindow to get them inserted into canvas __init__(self, parent, canv_w = 400, canv_h = 400, *args, **kwargs) docstring: Parent = master of scrolled window canv_w - width of canvas canv_h - height of canvas """ def __init__(self, parent, canv_w = 400, canv_h = 400, *args, **kwargs): """Parent = master of scrolled window canv_w - width of canvas canv_h - height of canvas """ super().__init__(parent, *args, **kwargs) self.parent = parent # creating a scrollbars self.xscrlbr = ttk.Scrollbar(self.parent, orient = 'horizontal') self.xscrlbr.grid(column = 0, row = 1, sticky = 'ew', columnspan = 2) self.yscrlbr = ttk.Scrollbar(self.parent) self.yscrlbr.grid(column = 1, row = 0, sticky = 'ns') # creating a canvas self.canv = tk.Canvas(self.parent) self.canv.config(relief = 'flat', width = 10, heigh = 10, bd = 2) # placing a canvas into frame self.canv.grid(column = 0, row = 0, sticky = 'nsew') # accociating scrollbar comands to canvas scroling self.xscrlbr.config(command = self.canv.xview) self.yscrlbr.config(command = self.canv.yview) # creating a frame to inserto to canvas self.scrollwindow = ttk.Frame(self.parent) self.canv.create_window(0, 0, window = self.scrollwindow, anchor = 'nw') self.canv.config(xscrollcommand = self.xscrlbr.set, yscrollcommand = self.yscrlbr.set, scrollregion = (0, 0, 100, 100)) self.yscrlbr.lift(self.scrollwindow) self.xscrlbr.lift(self.scrollwindow) self.scrollwindow.bind('<Configure>', self._configure_window) self.scrollwindow.bind('<Enter>', self._bound_to_mousewheel) self.scrollwindow.bind('<Leave>', self._unbound_to_mousewheel) return def _bound_to_mousewheel(self, event): self.canv.bind_all("<MouseWheel>", self._on_mousewheel) def _unbound_to_mousewheel(self, event): self.canv.unbind_all("<MouseWheel>") def _on_mousewheel(self, event): self.canv.yview_scroll(int(-1*(event.delta/120)), "units") def _configure_window(self, event): # update the scrollbars to match the size of the inner frame size = (self.scrollwindow.winfo_reqwidth(), self.scrollwindow.winfo_reqheight()) self.canv.config(scrollregion='0 0 %s %s' % size) if self.scrollwindow.winfo_reqwidth() != self.canv.winfo_width(): # update the canvas's width to fit the inner frame self.canv.config(width = self.scrollwindow.winfo_reqwidth()) if self.scrollwindow.winfo_reqheight() != self.canv.winfo_height(): # update the canvas's width to fit the inner frame self.canv.config(height = self.scrollwindow.winfo_reqheight())
For anyone who stumbles across this (as it did when looking for my own gist) I maintain a gist for exactly this purpose at https://gist.github.com/mp035/9f2027c3ef9172264532fcd6262f3b01 It has scrollwheel support for various operating systems, is commented, and has a built-in demo in the file.
We can add scroll bar even without using Canvas. I have read it in many other post we can't add vertical scroll bar in frame directly etc etc. But after doing many experiment found out way to add vertical as well as horizontal scroll bar :). Please find below code which is used to create scroll bar in treeView and frame. f = Tkinter.Frame(self.master,width=3) f.grid(row=2, column=0, columnspan=8, rowspan=10, pady=30, padx=30) f.config(width=5) self.tree = ttk.Treeview(f, selectmode="extended") scbHDirSel =tk.Scrollbar(f, orient=Tkinter.HORIZONTAL, command=self.tree.xview) scbVDirSel =tk.Scrollbar(f, orient=Tkinter.VERTICAL, command=self.tree.yview) self.tree.configure(yscrollcommand=scbVDirSel.set, xscrollcommand=scbHDirSel.set) self.tree["columns"] = (self.columnListOutput) self.tree.column("#0", width=40) self.tree.heading("#0", text='SrNo', anchor='w') self.tree.grid(row=2, column=0, sticky=Tkinter.NSEW,in_=f, columnspan=10, rowspan=10) scbVDirSel.grid(row=2, column=10, rowspan=10, sticky=Tkinter.NS, in_=f) scbHDirSel.grid(row=14, column=0, rowspan=2, sticky=Tkinter.EW,in_=f) f.rowconfigure(0, weight=1) f.columnconfigure(0, weight=1)
It is nessesery to configure Scrollbar in case of using with Canvas by sending to Canvas xscrollcommand attribute Scrollbar.set method and to Scrollbar command attribute Canvas.yview (xview) method. Canvas.yview method after scrollbar was moved recieve *args in next formatting: tuple('move_to', '<some_absolute_float_value_of_top_of_scrollbar_region>') In case of implementing scrollability to widget, Recieving region and translating scrollbar_region (whith element viewable and whith not) features must be created. Region is `tuple(float, float)' representing open to see part of all elements. Not ideal bechavior showed in this solution (without using tk.Canvas) import tkinter as tk from tkinter import ttk class ItemizeFrame(ttk.Frame, list): def __init__(self, *args, scroll_upd_callback = lambda x: x, visible_els: int = 10, **kwargs): list.__init__(self) ttk.Frame.__init__(self, *args, **kwargs) ttk.Style().configure('Small.TButton', background='red', width=2, height=2, padx=3, pady=3) ttk.Style().configure('Sep.TFrame', padx=3, pady=3) self.scroll_upd_callback = scroll_upd_callback self.visible_els = visible_els self.visible_st_idx = 0 self.pseudo_scroll_element_cursor_line = 0.5*1/visible_els def append(self, item: ttk.Widget, **kw): e = item(self, **kw) super().append(e) e.pack(fill='x') self._update_visible_els() def _update_visable_id_callback(self): for id_, entry_ in enumerate(self): entry_.set_id(id_) def pop(self, index=None): e = super().pop(index) e.destroy() self._update_visible_els() def __getitem__(self, idx) -> ttk.Widget: return list.__getitem__(self, idx) # indicators computing and application #property def visible_end_idx(self): return self.visible_st_idx + self.visible_els -1 #property def visible_area_ratio(self) -> tuple[float, float]: total = len(self) st_val = 0.0 end_val = 1.0 if total > self.visible_els: end_val = 1.0 - (total-self.visible_end_idx)/total st_val = self.visible_st_idx / total st_val = st_val + self.pseudo_scroll_element_cursor_line end_val = end_val + self.pseudo_scroll_element_cursor_line return (st_val, end_val) def _update_scroll_widget(self): self.scroll_upd_callback(*self.visible_area_ratio) def set_yview(self, move_to_ratio): base_pseudo_ratio = 0.5*1/self.visible_els total = len(self) max_ratio = (total - self.visible_els)/total+base_pseudo_ratio if move_to_ratio < 0: possible_st_el_pseudo_part = base_pseudo_ratio possible_st_el_idx = 0 if max_ratio < move_to_ratio: possible_st_el_idx = total - self.visible_els possible_st_el_pseudo_part = base_pseudo_ratio else : el_idx_raw = move_to_ratio * total el_idx_round = round(el_idx_raw) el_idx_pseudo = (el_idx_raw - el_idx_round)*1/self.visible_els possible_st_el_idx = el_idx_round possible_st_el_pseudo_part = el_idx_pseudo self.visible_st_idx = possible_st_el_idx self.pseudo_scroll_element_cursor_line = possible_st_el_pseudo_part self._update_visible_els() def _update_visible_els(self): for el in self: el.pack_forget() for num, el in enumerate(self): if self.visible_st_idx <= num and num <= self.visible_end_idx: el.pack() self._update_scroll_widget() class ScrollableFrame(ttk.Frame): def __init__(self, *args, **kwargs): kw = dict(width=400, height=300) kw.update(kwargs) super().__init__(*args, **kw) self.scroll = ttk.Scrollbar(self, command=self.on_scroll) self.scroll.pack(expand=True, fill='y', side='right') self.view = ItemizeFrame( self, scroll_upd_callback=self.scroll.set, **kwargs ) self.view.pack(expand=True, fill='both')#, side='left') def on_scroll(self, *args, **kwargs): value_raw = float(args[1]) self.view.set_yview(value_raw) Usecase class App(tk.Tk): def __init__(self): super().__init__() self.frame = ScrollableFrame(self) self.frame.pack() def test_fill(self): for i in range(15): self.frame.view.append(ttk.Entry) class Test: #staticmethod def v2(): app = App() app.test_fill() app.mainloop() Test.v2()
After I watching many answers, I got it: import tkinter as tk root = tk.Tk() root.title("音樂編輯器") root.geometry("600x480") def onFrameConfigure(canvas): '''Reset the scroll region to encompass the inner frame''' canvas.configure(scrollregion=canvas.bbox("all")) '''When window size change, canvas size will change, use this line to change its item size (width).''' canvas.itemconfigure(wrapFrame, width=canvas.winfo_width()) canvas = tk.Canvas(root, highlightthickness=0) frame = tk.Frame(canvas, background="#FFFFFF") vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview) canvas.configure(yscrollcommand=vsb.set) vsb.pack(side="right", fill="y") canvas.pack(fill="both", expand=1, anchor="nw") #canvas size is relative to window size. wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw") # When the window size change, it will call this function canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas)) L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25)) L1.pack(anchor="n") for i in range(100): input = tk.Entry(frame) input.pack() root.mainloop() Specifies the size of the scrollable frame by changing canvas and scrollbar position and size. import tkinter as tk root = tk.Tk() root.title("音樂編輯器") root.geometry("600x480") def onFrameConfigure(canvas): '''Reset the scroll region to encompass the inner frame''' canvas.configure(scrollregion=canvas.bbox("all")) canvas.itemconfigure(wrapFrame, width=canvas.winfo_width()) canvas = tk.Canvas(root, highlightthickness=0) frame = tk.Frame(canvas, background="#FFFFFF") vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview) canvas.configure(yscrollcommand=vsb.set) vsb.place(relx=0.9, y=0, relwidth=0.1, relheight=0.5) canvas.place(x=0, y=0, relwidth=0.9, relheight=0.5) wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw") canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas)) L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25)) L1.pack(anchor="n") for i in range(100): input = tk.Entry(frame) input.pack() root.mainloop() Specifies the size of the scrollable frame by writing them to outerFrame. import tkinter as tk root = tk.Tk() root.title("音樂編輯器") root.geometry("600x480") def onFrameConfigure(canvas): '''Reset the scroll region to encompass the inner frame''' canvas.configure(scrollregion=canvas.bbox("all")) canvas.itemconfigure(wrapFrame, width=canvas.winfo_width()) outerFrame = tk.Frame(root) canvas = tk.Canvas(outerFrame, highlightthickness=0) frame = tk.Frame(canvas, background="#FFFFFF") vsb = tk.Scrollbar(outerFrame, orient="vertical", command=canvas.yview) canvas.config(yscrollcommand=vsb.set) outerFrame.place(relx=0.25, rely=0.1, relwidth=0.5, relheight=0.5) vsb.pack(side="right", fill="y") canvas.pack(fill="both", expand=1, anchor="nw") wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw") canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas)) L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25)) L1.pack(anchor="n") for i in range(100): input = tk.Entry(frame) input.pack() root.mainloop() The items inner the frame can use pack or grid (only choose one), but place cannot be used alone. If you want to use place, you need to expand the layout(height) with pack or grid first. import tkinter as tk root = tk.Tk() root.title("音樂編輯器") root.geometry("600x480") def onFrameConfigure(canvas): '''Reset the scroll region to encompass the inner frame''' canvas.configure(scrollregion=canvas.bbox("all")) canvas.itemconfigure(wrapFrame, width=canvas.winfo_width()) canvas = tk.Canvas(root, highlightthickness=0) frame = tk.Frame(canvas, background="#FFFFFF") vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview) canvas.configure(yscrollcommand=vsb.set) vsb.pack(side="right", fill="y") canvas.pack(fill="both", expand=1, anchor="nw") wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw") canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas)) L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25)) L1.pack(anchor="n") for i in range(100): input = tk.Entry(frame) input.pack() L1 = tk.Label(frame, text="我是Label") L1.place(x=0, rely=0.5) root.mainloop() Use mouse wheel: tkinter: binding mousewheel to scrollbar import tkinter as tk root = tk.Tk() root.title("音樂編輯器") root.geometry("600x480") def onFrameConfigure(canvas): canvas.configure(scrollregion=canvas.bbox("all")) canvas.itemconfigure(wrapFrame, width=canvas.winfo_width()) def on_mouse_wheel(event, scale=3): #only care event.delta is - or +, scroll down or up if event.delta<0: canvas.yview_scroll(scale, "units") else: canvas.yview_scroll(-scale, "units") canvas = tk.Canvas(root, highlightthickness=0) frame = tk.Frame(canvas, background="#FFFFFF") vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview) canvas.configure(yscrollcommand=vsb.set) vsb.pack(side="right", fill="y") canvas.pack(fill="both", expand=1, anchor="nw") wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw") canvas.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas)) canvas.bind("<Enter>", lambda event: canvas.bind_all("<MouseWheel>", on_mouse_wheel)) # on mouse enter canvas.bind("<Leave>", lambda event: canvas.unbind_all("<MouseWheel>")) # on mouse leave L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25)) L1.pack(anchor="n") for i in range(100): input = tk.Entry(frame) input.pack() root.mainloop() Export to class: import tkinter as tk root = tk.Tk() root.title("音樂編輯器") root.geometry("600x480") class scrollFrame(): def __init__(self, **options): outerFrame = tk.Frame(root) canvas = tk.Canvas(outerFrame, highlightthickness=0) vsb = tk.Scrollbar(outerFrame, orient="vertical", command=canvas.yview) vsb.pack(side="right", fill="y") canvas.pack(fill="both", expand=1, anchor="nw") frame = tk.Frame(canvas, **options) wrapFrameId = canvas.create_window((0,0), window=frame, anchor="nw") canvas.config(yscrollcommand=vsb.set) canvas.bind("<Configure>", lambda event: self.onFrameConfigure()) canvas.bind("<Enter>", lambda event: canvas.bind_all("<MouseWheel>", self.on_mouse_wheel)) # on mouse enter canvas.bind("<Leave>", lambda event: canvas.unbind_all("<MouseWheel>")) # on mouse leave self.outerFrame, self.canvas, self.vsb, self.frame, self.wrapFrameId = outerFrame, canvas, vsb, frame, wrapFrameId def onFrameConfigure(self): canvas = self.canvas '''Reset the scroll region to encompass the inner frame''' canvas.configure(scrollregion=canvas.bbox("all")) canvas.itemconfigure(self.wrapFrameId, width=canvas.winfo_width()) def on_mouse_wheel(self, event, scale=3): canvas = self.canvas #only care event.delta is - or +, scroll down or up if event.delta<0: canvas.yview_scroll(scale, "units") else: canvas.yview_scroll(-scale, "units") frame = scrollFrame(background="#FFFFFF") frame.outerFrame.place(relx=0.15, rely=0.1, relwidth=0.7, relheight=0.8) L1 = tk.Label(frame.frame, text="音樂編輯器", bg="#556644", font=("",25)) L1.pack(anchor="n") for i in range(100): input = tk.Entry(frame.frame) input.pack() root.mainloop() According: https://stackoverflow.com/a/3092341/19470749 https://stackoverflow.com/a/16198198/19470749 https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/create_window.html
Not 100% sure if this solution is on topic (since it explicitely asks for a scrollable FRAME), but the text widget is basically a scrollable Frame. From documentation of the Text widget: "Like canvas widgets, text widgets can contain images and any other Tk widgets (including frames containing many other widgets). In a sense, this allows the text widget to work as a geometry manager in its own right. " Text widgets are very easy to use, and can be made scrollable. So instead of using a special Class like the Scrollable Frame, I think the Text widget is a great option. Below my code, for a basic example of a scrollable text widget holding 100 buttons: from tkinter import Tk, Button, Text,Scrollbar class test: def __init__(self): self.win = Tk() text = Text(self.win, width=40, height=10, wrap = "none") ys = Scrollbar(self.win, orient = 'vertical', command = text.yview) text['yscrollcommand'] = ys.set text.grid(column = 0, row = 0, sticky = 'nwes') ys.grid(column = 1, row = 0, sticky = 'ns') for x in range(1,100): b = Button(text, text='Push Me') text.window_create("end", window=b) text.insert("end",'\n') self.win.mainloop() test = test() This is at least the method I am going to use for my scrollable frames. Not sure if there is a better solution then the newline insertion to make the widgets organised vertically. But it works.