92

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()
  1. Am I doing it right? Is there better/smarter way to achieve the output this code gave me?
  2. Why must I use grid method? (I tried place method, but none of the labels appear on the canvas.)
  3. What so special about using anchor='nw' when creating window on canvas?

Please keep your answer simple, as I am a beginner.

martineau
  • 119,623
  • 25
  • 170
  • 301
Chris Aung
  • 9,152
  • 33
  • 82
  • 127
  • 2
    You have it backwards in your question, though the code looks correct at first glance. You must create a frame, embed that in the canvas, then attach the scrollbar to the canvas. – Bryan Oakley Apr 24 '13 at 11:07
  • 4
    possible duplicate of [Adding a scrollbar to a grid of widgets in Tkinter](http://stackoverflow.com/questions/3085696/adding-a-scrollbar-to-a-grid-of-widgets-in-tkinter) – Trevor Boyd Smith Aug 26 '13 at 15:56
  • @TrevorBoydSmith There's a lot of stuff this is a potential duplicate of, but I voted to close this as a duplicate of a different one that seems to have the best answers: http://stackoverflow.com/questions/1873575/how-could-i-get-a-frame-with-a-scrollbar-in-tkinter – ArtOfWarfare May 03 '15 at 15:59
  • 1
    I'm extremely late, but thank you so much for this! This was the only fully-functional (and complete) example of creating a scrollable frame using only pure Tkinter (a restriction for my project). I know that wasn't your intention, but thank you! – Niema Moshiri Apr 12 '18 at 06:33
  • When I place this frame as is inside another frame and use grid to draw it it somehow gets much bigger than allowed. How can I ensure that the canvas stays inside the borders of its parent frame? – Druckermann Jul 20 '20 at 13:13

8 Answers8

60

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.

martineau
  • 119,623
  • 25
  • 170
  • 301
Gonzo
  • 2,023
  • 3
  • 21
  • 30
  • even though you are not answering my question, thanks for your example.But i am reluctant to write that many lines of code just for a frame with scroll bar when my entire script is shorter than your example.In fact my sample code will do the trick though i have no idea why i can't use place method. – Chris Aung Apr 25 '13 at 08:29
  • 2
    @ChrisAung: The nice thing about this solution is that it has a reusable class, `VerticalScrolledFrame`, which you can use as a replacement for any `Frame`. Going with the code in your solution, you need to rewrite all of that code for every single `Frame` which you want to be able to scroll. With this code, it's nearly a drop-in replacement for `Frame`. So don't be reluctant to use it on account of the line count. His solution is more code if you only need a single scrollable frame. If you need two, both techniques take an equal amount of code, and his is more maintainable (less redundant.) – ArtOfWarfare May 02 '15 at 15:15
  • Continuing from above: If you need more than 2 scrollable frames, his solution takes far less code, and is much easier to maintain. Having said all that, I wouldn't use this solution on account of the caveats it has. Also, I dislike the requirement to use `.interior` - I would want an interface that makes `.interior` an implementation detail instead of making it something the person using it has to know about. – ArtOfWarfare May 02 '15 at 15:18
  • This is the best response I found on the matter, but it doesn't exactly work as is. The scrollbar is certainly there, its size fit, but it doesn't scroll the view. I've investigated a bit and found out that it'll only work out if you create the window after populating the hosted frame. So you'd have to split the init part by adding another callable method that would create the window after populating the internal frame. Like this: `def create(self): self.interior_id = self.canvas.create_window(0, 0, window=self.interior, anchor=NW, tags="self.interior")` – Olivier B. Sep 14 '22 at 10:22
  • Regarding the mouse wheel scrolling, you need to bind it using `self.canvas.bind_all("", self._on_mousewheel)` in the `__init__` method, and then scroll accordingly: `def _on_mousewheel(self, event): delta = (event.delta/120) if is_windows() else event.delta self.canvas.yview_scroll(-1*delta, "units")` as explained [here](https://stackoverflow.com/questions/17355902/tkinter-binding-mousewheel-to-scrollbar). I know this is 7 years old stuff, but that was my today's issue, so... hope it helps. – Olivier B. Sep 14 '22 at 10:25
36

"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.

Delrius Euphoria
  • 14,910
  • 3
  • 15
  • 46
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
18

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())
Mikhail T.
  • 1,240
  • 12
  • 21
  • 2
    small bug, your last line should read `self.canv.config(height= ...)` – Adam Smith Jul 21 '16 at 20:22
  • What do you mean, "make this frame a child of my ScrolledWindow.scrollwindow". Can you give an example? – Jacob Jan 29 '18 at 22:05
  • @Jacob the usage is basically assign the Scrolled window: `self.sw = ScrolledWindow(self.root)` then you would assign any children as `self.child(self.sw.scrollwindow, any of the child's options` so to add a button to it would be `self.btn = tk.Button(self.sw.scrollwindow, text = 'Button', command = lambda: print("Button Pressed"))` – tgikal Nov 07 '18 at 18:01
  • @MikhailT. I modified this a bit to fit my needs, but this was a great help in replacing the Scrolled Frame in Pmw, since apparently Pmw's weird import loading is incompatable with pyinstaller, thank you. – tgikal Nov 07 '18 at 18:04
  • By far the BEST answer as of Oct 2021. Perfectly solved my problem while the currently highest voted answer could not handle cases where the requested size of the frame changes constantly. Thank you so much! – Billy Cao Oct 19 '21 at 18:03
  • It would be nice to have complete code to show how to invoke this function. What can I use for the "tk.Frame" argument to get this to run? – Rich Lysakowski PhD Oct 20 '21 at 04:36
  • 4
    The frame grows with the widgets packed inside it, and never scrolls. Resizing the window doesn't resize the size of the canvas. – Cris Luengo Nov 11 '21 at 20:41
4

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.

mp035
  • 964
  • 7
  • 18
  • 1
    I can highly recommend that implementation. It's also easily usable with a ttk frame and scrollbar. – Timo Jan 05 '23 at 21:50
2

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)
Biyau
  • 121
  • 1
  • 12
  • 3
    I don't understand these answers' use of "Tkinter", "tk" and "ttk". I would expect just one such prefix in each example, but the third has all three. What gives? – 4dummies Dec 16 '17 at 22:14
  • 3
    He would have to have started this code with`import Tkinter`, `import Tkinter as tk` and `import ttk`. The first two are redundant, so all of the places he has `Tkinter.NSEW` for example, he could just put `tk.NSEW`.`ttk` is its own module, and the Treeview is a class that only exists in that module, so that statement is the way it should be. – Todd Oct 03 '18 at 00:47
  • @4dummies : "tk" is a synonym for "Tkinter", created using the "as" keyword when importing. Generally, most people use "Tkinter" for tutorials, and "tk" in their own code. It is possible that he copy & pasted different projects together. Regardless, ttk is a seperate module, an implementation of various higher-level widgets & styling. The import is correct, as it is its own module. – The Daleks stand with Ukraine Dec 21 '19 at 19:17
  • 3
    How do I run this code solution? I get an error message "NameError: name 'self' is not defined" Please provide a full working example that creates the treeview and frame. Thank you. – Rich Lysakowski PhD Oct 20 '21 at 04:35
0

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()
xillmera
  • 22
  • 3
0

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

謝咏辰
  • 37
  • 4
0

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.