Python Forum
[Tkinter] How to create scrollable frame
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] How to create scrollable frame
#1
I've been using this framework Multipage Framework for developing an app... I'm kinda new to tkinter and I just finished developing a beta version and I just noticed that frames are not native scrollable. I checked some tutorials online on how to make them scrollable creating canvas inside or using ScrolledFrame package...). So, I tried with the scrolled frame package but it doesn't work right (like a normal app should): basically the content doesn't resize with the window. So is here anyone kind to help me and improve the basic structure of the framework in the link to make frames scrollables? Here's how I managed to create it scrollable but loosing "content-auto-fitting" capabilities:

import tkinter as tk                # python 3
from tkinter import font as tkfont  # python 3

class SampleApp(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")
        
        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        scrollable_frame = ScrollableFrame(container, bg="red")
        scrollable_frame.pack(fill="both", expand=True)
  
        scrollable_frame.grid_rowconfigure(0, weight=1)
        scrollable_frame.grid_columnconfigure(0, weight=1)
        

        self.frames = {}
        for F in (StartPage, PageOne, PageTwo):
            page_name = F.__name__
            frame = F(parent=scrollable_frame, controller=self)
            self.frames[page_name] = frame

            # put all of the pages in the same location;
            # the one on the top of the stacking order
            # will be the one that is visible.
            frame.grid(row=0, column=0, sticky="nsew")
            frame.grid_rowconfigure(0, weight=1)
            frame.grid_columnconfigure(0, weight=1)
            frame.config(highlightbackground="green", highlightcolor="green", highlightthickness=1)

      
        
        
        self.show_frame("StartPage")

    def show_frame(self, page_name):
        #Show a frame for the given page name
        frame = self.frames[page_name]
        frame.tkraise()


    FIT_WIDTH = "fit_width"
    FIT_HEIGHT = "fit_height"


class ScrollableFrame(tk.Frame):
    """
    There is no way to scroll <tkinter.Frame> so we are
    going to create a canvas and place the frame there.
    Scrolling the canvas will give the illution of scrolling
    the frame
    Partly taken from:
        https://blog.tecladocode.com/tkinter-scrollable-frames/
        https://stackoverflow.com/a/17457843/11106801

    master_frame---------------------------------------------------------
    | dummy_canvas-----------------------------------------  y_scroll--  |
    | | self---------------------------------------------  | |         | |
    | | |                                                | | |         | |
    | | |                                                | | |         | |
    | | |                                                | | |         | |
    | |  ------------------------------------------------  | |         | |
    |  ----------------------------------------------------  |         | |
    |                                                        |         | |
    | x_scroll---------------------------------------------  |         | |
    | |                                                    | |         | |
    |  ----------------------------------------------------   ---------  |
     --------------------------------------------------------------------
    """
    def __init__(self, master=None, scroll_speed=2,
                 hscroll=False, vscroll=True, **kwargs):
        assert isinstance(scroll_speed, int), "`scroll_speed` must be an int"
        self.scroll_speed = scroll_speed

        self.master_frame = tk.Frame(master)
        self.dummy_canvas = tk.Canvas(self.master_frame, **kwargs)
        super().__init__(self.dummy_canvas)

        # Create the 2 scrollbars
        if vscroll:
            self.v_scrollbar = tk.Scrollbar(self.master_frame,
                                            orient="vertical",
                                            command=self.dummy_canvas.yview)
            self.v_scrollbar.pack(side="right", fill="y")
            self.dummy_canvas.configure(yscrollcommand=self.v_scrollbar.set)
        if hscroll:
            self.h_scrollbar = tk.Scrollbar(self.master_frame,
                                            orient="horizontal",
                                            command=self.dummy_canvas.xview)
            self.h_scrollbar.pack(side="bottom", fill="x")
            self.dummy_canvas.configure(xscrollcommand=self.h_scrollbar.set)

        # Bind to the mousewheel scrolling
        self.dummy_canvas.bind_all("<MouseWheel>", self.scrolling_windows,
                                   add=True)
        self.dummy_canvas.bind_all("<Button-4>", self.scrolling_linux, add=True)
        self.dummy_canvas.bind_all("<Button-5>", self.scrolling_linux, add=True)
        self.bind("<Configure>", self.scrollbar_scrolling, add=True)

        # Place `self` inside `dummy_canvas`
        self.dummy_canvas.create_window((0, 0), window=self, anchor="nw")
        # Place `dummy_canvas` inside `master_frame`
        self.dummy_canvas.pack(side="top", expand=True, fill="both")

        self.pack = self.master_frame.pack
        self.grid = self.master_frame.grid
        self.place = self.master_frame.place
        self.pack_forget = self.master_frame.pack_forget
        self.grid_forget = self.master_frame.grid_forget
        self.place_forget = self.master_frame.place_forget

    def scrolling_windows(self, event):
        assert event.delta != 0, "On Windows, `event.delta` should never be 0"
        y_steps = int(-event.delta/abs(event.delta)*self.scroll_speed)
        self.dummy_canvas.yview_scroll(y_steps, "units")

    def scrolling_linux(self, event):
        y_steps = self.scroll_speed
        if event.num == 4:
            y_steps *= -1
        self.dummy_canvas.yview_scroll(y_steps, "units")

    def scrollbar_scrolling(self, event):
        region = list(self.dummy_canvas.bbox("all"))
        region[2] = max(self.dummy_canvas.winfo_width(), region[2])
        region[3] = max(self.dummy_canvas.winfo_height(), region[3])
        self.dummy_canvas.configure(scrollregion=region)

    def resize(self, fit=None, height=None, width=None):
        if fit == FIT_WIDTH:
            super().update()
            self.dummy_canvas.config(width=super().winfo_width())
        if fit == FIT_HEIGHT:
            super().update()
            self.dummy_canvas.config(height=super().winfo_height())
        if height is not None:
            self.dummy_canvas.config(height=height)
        if width is not None:
            self.dummy_canvas.config(width=width)
    fit = resize

class StartPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is the start page", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)

        button1 = tk.Button(self, text="Go to Page One",
                            command=lambda: controller.show_frame("PageOne"))
        button2 = tk.Button(self, text="Go to Page Two",
                            command=lambda: controller.show_frame("PageTwo"))
        button1.pack()
        button2.pack()
       

class PageOne(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is page 1", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)
        for i in range(0,99):
            button = tk.Button(self, text="Go to the start page",
                           command=lambda: controller.show_frame("StartPage"))
            button.pack()

class PageTwo(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is page 2", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)
        button = tk.Button(self, text="Go to the start page",
                           command=lambda: controller.show_frame("StartPage"))
        button.pack()

if __name__ == "__main__":
    
    app = SampleApp()
    app.mainloop()
Reply
#2
Can you do resizing without the "scrollable" part?
Reply
#3
(Aug-06-2021, 06:38 PM)deanhystad Wrote: Can you do resizing without the "scrollable" part?
I honestly don't know what you mean xD but I updated the code to my latest version. If you run it you can see that in red is the scrollable frame and with the green border is the class page frame. I only need the page to fill all the frame and to resize accordingly to it
Reply
#4
I am trying to understand if this is a "I don't know how to make tkinter resize to fit the window" or "I don't know how to make the canvas resize my frame" problem.
Reply
#5
(Aug-06-2021, 07:28 PM)deanhystad Wrote: I am trying to understand if this is a "I don't know how to make tkinter resize to fit the window" or "I don't know how to make the canvas resize my frame" problem.

It's a I'm a newbie problem xD No, seriously, I started coding using a multi page framweork i found online... But only later i discovered frames are not resizable. So now I'm addressing the problem. I have a class ScrollableFrame which uses the "canvas" trick to make a frame scrollable. It's a class so that it is reusable. So now basically I can instantiate a ScrollableFrame (which i need it in the SampleApp Class). I also used a container (a frame which i coloured red) which wraps the Scrollable frame. So now In the for loop I Instantiate all the pages I have created as classes. But if you run the code you can see that the Scrollable Frame doesn't expand to fit its container. That is the problem. This result in the fact that if i resize the container, the Scrollable Frame doesnt resize.

In short: I cant't manage to make the ScrollableFrame to fill all the space it can
Reply
#6
Rewrite your example to not have the scrollable frame, just the two buttons. Can you write your program so the buttons expand to fill the window?
Reply
#7
(Aug-07-2021, 05:39 AM)deanhystad Wrote: Rewrite your example to not have the scrollable frame, just the two buttons. Can you write your program so the buttons expand to fill the window?

It does that if in the for loop i set Parent= container instead of parent = scrollable_frame. But this way I lose the ability to scroll the frames.
If you run the program you can see a frame with green border: that's the frame in the loop. Why doesn't it resize to all the scrollable frame (red background)? 😂
Reply
#8
You don't call super().__init__() or tk.Frame.__init__(self) in your ScrollableFrame.__init__() method. I noticed this when I tried to bind a function to the <configure> event. The frame doesn't have a window, so the request failed. There's probably many oddities occurring because your Scrollable frame is only partially a frame.

What is the purpose of the master_frame? You need a frame in which to place the canvas and scrollbars, but this should be the ScrollableFrame (self). Get rid of the master_frame and make the ScrollableFrame the parent of the canvas. This will clean up a lot of ugly in your code.
        self.pack = self.master_frame.pack  # Yuck!
        self.grid = self.master_frame.grid
        self.place = self.master_frame.place
        self.pack_forget = self.master_frame.pack_forget
        self.grid_forget = self.master_frame.grid_forget
        self.place_forget = self.master_frame.place_forget
Why is your ScrollableFrame height so large? The ScrollableFrame height should be the height of what is currently the master_frame.

You should not have to specify hscroll or vscroll. If the scrolling window is wider than the viewport you should display a horizontal scrollbar. If the scrolling window is taller than the viewport you should display a vertical scrollbar. If your scrolling window is smaller than the viewport there should be no scrollbars and you might want to expand the scrolling window to fill the viewport.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyQt] PyQt5 drawing on scrollable area HeinKurz 3 1,250 Mar-28-2023, 12:58 PM
Last Post: Axel_Erfurt
  [Tkinter] Scrollable buttons with an add/delete button Clich3 5 3,331 Jun-16-2022, 07:19 PM
Last Post: rob101
Question [Tkinter] Scrollable Treeview: change behavior of Prior/Next keys? RockDoctor 2 3,154 Apr-10-2021, 05:40 PM
Last Post: RockDoctor
  Scrollable big image in a window (TKinter) Prospekteur 3 4,408 Sep-14-2020, 03:06 AM
Last Post: Larz60+
  [Tkinter] Scrollbar, Frame and size of Frame Maksim 2 8,939 Sep-30-2019, 07:30 AM
Last Post: Maksim
  [Tkinter] create and insert a new frame on top of another frame atlass218 4 10,991 Apr-18-2019, 05:36 PM
Last Post: atlass218
  [Tkinter] Fixate graphs on scrollable canvas janema 6 5,353 Apr-12-2019, 03:57 PM
Last Post: Larz60+
  [Tkinter] ListBox not contained or scrollable kellykimble 6 5,097 Apr-05-2018, 11:59 AM
Last Post: Larz60+
  [Tkinter] Frame size only works if frame is empty(Solved) Tuck12173 7 6,367 Jan-29-2018, 10:44 PM
Last Post: Larz60+

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020