How to create a Dynamic Scrollable Grid on tkinter - python

When I say a dynamic grid I'm saying a grid that adjusts the number of columns(like bootstrap) according to the window width, so it must adjust the "cards" accordingly, and when I say scrollable... well... easier to understand.
I've tried 2 aproachs:
create a dynamic grid and then make it scrollable.
create a scrollable grid and then make it dynamic.
I've failed in both ways! I've found out that for being scrollable the grid can't be in a simple frame, it must be in a Canvas. And for the canvas I'm having a hard time making it dynamic.
Down here is my dynamic grid code
class DynaGrid(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
self.columns = None
self.bind('<Configure>', self.re_grid)
def re_grid(self, event=None):
grid_width = self.winfo_width()
slaves = self.grid_slaves()
slaves_width = slaves[1].winfo_width()
cols = grid_width // slaves_width
if (cols == self.columns) | (cols == 0):
return
for i, slave in enumerate(reversed(slaves)):
slave.grid_forget()
slave.grid(row=i // cols, column=i % cols)
self.columns = cols
class CardFrame(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, bd=1, relief=tk.RAISED, **kwargs)
tk.Label(self, text="Hello").pack()
def main():
root = tk.Tk()
frame = DynaGrid(root)
frame.pack(fill=tk.BOTH, expand=True)
CardFrame(frame).grid()
CardFrame(frame).grid()
CardFrame(frame).grid()
CardFrame(frame).grid()
CardFrame(frame).grid()
CardFrame(frame).grid()
CardFrame(frame).grid()
CardFrame(frame).grid()
root.mainloop()
if __name__ == '__main__':
main()
I wont waste other people's time posting my messy canvas code here instead I've got one from
https://blog.tecladocode.com/tkinter-scrollable-frames/
which I've made the change to use my "cards" instead of labels.
import tkinter as tk
from tkinter import ttk
class ScrollableFrame(ttk.Frame):
def __init__(self, container, *args, **kwargs):
super().__init__(container, *args, **kwargs)
canvas = tk.Canvas(self)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
self.scrollable_frame = ttk.Frame(canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(
scrollregion=canvas.bbox("all")
)
)
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
class TestFrame(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, bd=5, relief=tk.RAISED, **kwargs)
tk.Label(self, text="name").pack(pady=10)
root = tk.Tk()
frame = ScrollableFrame(root)
for i in range(50):
TestFrame(frame.scrollable_frame).grid()
frame.pack()
root.mainloop()
To make the dynamic scrollable canvas the tricky part here is use the re_grid function inside the canvas. I'm lost in how I'll get the window width correctly like I did in the dynaGrid code.
In the end I want a mash up of these two codes; a Class that is some sort of frame with dynamic grid with lateral scroll.

Your canvas is a frame inside your class,
so, what you need to do is bind your re-grid function to your master frame and keep the scrollable frame config callback as it is. Then alter the grid, inside your canvas, instead of your 'dynamic grid' from your re-grid function . Simple as that!
And I think it was easier to say: I want to create a window that behaves like a File Explorer
import tkinter as tk
from tkinter import ttk
class ScrollableFrame(ttk.Frame):
def _init_(self, container, *args, **kwargs):
super()._init_(container, *args, **kwargs)
canvas = tk.Canvas(self)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
self.scrollable_frame = ttk.Frame(canvas)
self.columns=0
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(
scrollregion=canvas.bbox("all")
)
)
self.bind('<Configure>', self.regrid)
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def regrid(self, event=None):
print(type(self))
grid_width = self.winfo_width()
slaves = self.scrollable_frame.grid_slaves()
print(len(slaves))
slaves_width = slaves[1].winfo_width()
cols = grid_width // slaves_width
if (cols == self.columns) | (cols == 0): # if the column number has not changed, abort
return
for i, slave in enumerate(reversed(slaves)):
slave.grid_forget()
slave.grid(row=i // cols, column=i % cols)
self.columns = cols
class TestFrame(tk.Frame):
def _init_(self, master=None, **kwargs):
tk.Frame._init_(self, master, bd=5, relief=tk.RAISED, **kwargs)
tk.Label(self, text="name").pack(pady=10)
tk.Label(self, text=" info ........ info ").pack(pady=10)
root = tk.Tk()
frame = ScrollableFrame(root)
for i in range(10):
TestFrame(frame.scrollable_frame).grid()
TestFrame(frame.scrollable_frame).grid()
frame.pack(side="left", fill=tk.BOTH, expand=True)
root.mainloop()

Related

how to make a scrollable frame in tkinter python?

"""
this class will allow you to make a scrollable frame
to pack or grid use self.outer_frame you can make the outer frame packed in master in the init
method by settingouter_pack = True
i have seen alot of codes related to this but for some reasons it does not work for me
the class i made will work with the mouse wheal and place
the scroll bar is there if you want to show it then pack it
"""
class ScrollableFrame(Frame):
def __init__(self, master, outer_pack: bool=False, *args, **kwargs):
#create the main frame
self.outer_frame = Frame(master, width=50)
if outer_pack:
self.outer_frame.pack()
#create the canvas
self.canvas = Canvas(self.outer_frame, bg="white")
super(ScrollableFrame, self).__init__(master=self.canvas, *args, **kwargs)
self.canvas.pack(expand=0)
#add a scroll bar to the canvas
self.scroll_bar = ttk.Scrollbar(self.outer_frame, orient=VERTICAL, command=self.canvas.yview)
#configure the canvas
self.canvas.configure(yscrollcommand=self.scroll_bar.set)
self.canvas.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
self.canvas.bind_all("<MouseWheel>", lambda e: self.__on_mousewheel(e))
#put self in a window in the canvas
self.pack(fill=X, expand=1, side=RIGHT)
self.canvas.create_window((0, 0), window=self, anchor="e")
def __on_mousewheel(self, event):
self.canvas.yview_scroll(-1 * (event.delta // 120), "units")

Tkinter add widget on scroolable frame last item does not shown

I want to create a window that allows entering one-to-many fields for the file transfer.
I created a Scrollable Frame and I am adding Entry-Text pairs in runtime. If I click the button for the first time, everything goes well. After the second time, nothing happens on the UI side. It works perfectly after the second click. But I saw that all pairs added successfully, just the UI did not display it. Does anybody know how to fix it?
import tkinter as tk
class VerticalScroolFrame(tk.Frame):
"""A frame with a vertical scroolbar"""
def __init__(self, master, *args, **kwargs):
main_frame = tk.Frame(master)
main_frame.grid()
main_frame.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
self._canvas = tk.Canvas(main_frame)
self._canvas.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar = tk.Scrollbar(main_frame, command=self._canvas.yview)
scrollbar.grid(row=0, column=1, sticky=tk.N+tk.S+tk.W)
self._canvas.configure(yscrollcommand=scrollbar.set)
self._canvas.bind('<Configure>', lambda *_: self.on_configure())
super().__init__(self._canvas, *args, **kwargs)
self._canvas.create_window((0, 0), window=self, anchor=tk.NW)
def on_configure(self):
"""update scrollregion after starting 'mainloop'
when all widgets are in self._canvas. And also need to be triggered
whenever a widget added as a child.
"""
self._canvas.configure(scrollregion=self._canvas.bbox('all'))
class AdvancedTransfer:
"""Opens a window that allows you to enter source file list
and targets for them. One-to-many relation.
"""
def __init__(self, root):
self._scroolable_frame = VerticalScroolFrame(root)
self._entry_text_dict = {}
self._button = tk.Button(root, text="Add", command=self.add_item)
self._button.grid()
def add_item(self):
"""Add entry-text widget group"""
row = len(self._entry_text_dict)
entry = tk.Entry(self._scroolable_frame)
entry.insert(0, "row number: {0}".format(row))
entry.grid(row=row, column=0)
text = tk.Text(self._scroolable_frame)
text.grid(row=row, column=1)
self._entry_text_dict[entry] = text
self._scroolable_frame.on_configure()
def main():
root = tk.Tk()
main_frame = tk.Frame(root)
main_frame.grid()
AdvancedTransfer(main_frame)
root.mainloop()
if __name__ == "__main__":
main()
Repro steps:
Run the code below.
Click the button two times.
You should see 2 pairs but only 1 pair shown instead.
It is because you bind <Configure> event on wrong widget. You should bind on the internal frame (i.e. instance of VerticalScroolFrame) instead of canvas (self._canvas):
class VerticalScroolFrame(tk.Frame):
"""A frame with a vertical scroolbar"""
def __init__(self, master, *args, **kwargs):
main_frame = tk.Frame(master)
main_frame.grid()
main_frame.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
self._canvas = tk.Canvas(main_frame)
self._canvas.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar = tk.Scrollbar(main_frame, command=self._canvas.yview)
scrollbar.grid(row=0, column=1, sticky=tk.N+tk.S+tk.W)
self._canvas.configure(yscrollcommand=scrollbar.set)
#self._canvas.bind('<Configure>', lambda *_: self.on_configure())
super().__init__(self._canvas, *args, **kwargs)
self._canvas.create_window((0, 0), window=self, anchor=tk.NW)
# bind <Configure> event on itself
self.bind('<Configure>', lambda _: self.on_configure())

Frame containing a scrollable canvas is not taking up the rest of the allotted Tk window

I have written an application that allows users to select a directory and load the info in the directory. The user can then select which aspects of the files to display in a figure. The figure is placed in a Tkinter-matplotlib canvas which is within a canvas window that the user can scroll. The issue I am having is that the frame (canvas_frame in StartPage) containing the scrollable frame doesn't take the allotted space in the Tkinter window.
The code below replicates the issue and the the image is what the application looks like. Most of the code for the scrollable frame was taken from ex. 1 and ex. 2. An image of the application is here:
https://imgur.com/ELXmehG
from tkinter import Tk, Frame, Canvas
from tkinter.ttk import Scrollbar
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg as FigCanvas
from matplotlib.figure import Figure
class Scrollable(Frame):
def __init__(self, frame, fig, width=16):
# Base class initialization
Frame.__init__(self, frame)
# Instance variable for tkinter canvas
self.tk_cnv = Canvas(frame, highlightthickness=0)
self.tk_cnv.pack(side='left', fill='both', expand=True)
# Instance variable for the scroll-bar
v_scroll = Scrollbar(frame, width=width)
v_scroll.pack(side="right", fill="y", expand=False)
v_scroll.config(command=self.tk_cnv.yview)
v_scroll.activate(" ")
# Instance variable for the matplotlib canvas
self.mpl_cnv = FigCanvas(fig, frame)
self.cnv_widget = self.mpl_cnv.get_tk_widget()
self.cnv_widget.config(yscrollcommand=v_scroll.set)
self.cnv_widget.bind("<Configure>", self.__fill_canvas)
# Assign frame generated by the class to the canvas
# and create a scrollable window for it.
self.windows_item = \
self.tk_cnv.create_window((0, 900), window=self.cnv_widget, anchor='e',
tag='self.canvas')
self.tk_cnv.config(scrollregion=self.tk_cnv.bbox("all"))
def __fill_canvas(self, event):
# Enlarge the windows item to the canvas width
canvas_width = event.width
canvas_height = event.height
self.tk_cnv.itemconfig(self.windows_item, width=canvas_width,
height=canvas_height)
class StartPage(Frame):
""" Tkinter based class for single frame upon which widgets
such as buttons, check-buttons, and entry are used as a
simple graphical user interface.
"""
LARGE_FONT = ("Veranda", 12)
def __init__(self, parent, controller):
Frame.__init__(self, parent)
# Instance variables with page/window info of current frame
self.window = parent
# Instance variable for third row of widgets
self.canvas_frame = Frame(self.window, relief="sunken")
self.canvas_frame.grid(row=0, column=0, pady=5, sticky="news")
# Instance variables for the figure
self.plot_fig = Figure(figsize=[14.0, 18.0])
# Instance variable for the frame with scrolling functionality
self.canvas_body = Scrollable(self.canvas_frame, self.plot_fig)
self.canvas = self.canvas_body.mpl_cnv
self.canvas_setup()
def canvas_setup(self):
self.canvas_frame.grid_rowconfigure(2, weight=1)
self.canvas_frame.grid_columnconfigure(0, weight=1)
class MainContainer(Tk):
""" Tkinter based class used to generate a single window
and contain a single frame. The frame contains multiple
widgets for user choice and visualization.
"""
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
Tk.wm_title(self, "Sequence Viewer")
Tk.wm_resizable(self, width=True, height=True)
container = Frame(self)
container.grid_configure(row=0, column=0, sticky="nsew")
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
frame = StartPage(container, self)
self.frames[StartPage] = frame
self.show_frame(StartPage)
self.center_window()
def show_frame(self, frame_to_add):
frame = self.frames[frame_to_add]
frame.tkraise()
def center_window(self):
w = 1100
h = 900
sw = self.winfo_screenwidth()
sh = self.winfo_screenheight()
x = (sw - w) / 2
y = (sh - h) / 2
self.geometry('%dx%d+%d+%d' % (w, h, x, y))
if __name__ == "__main__":
app = MainContainer()
app.mainloop()
The major cause of the issue was the use of the initial Frame object from the MainContainer class to instantiate the Frame in which the Scrollable class is contained. The object of the MainContainer is what should have been passed, this is because it inherits from the Tk class.
Secondly, the mixture of window managers was causing problems. Because of this I switched to pack exclusively. The solution is below.
from tkinter import Tk, Frame, Canvas
from tkinter.ttk import Scrollbar
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg as FigCanvas
from matplotlib.figure import Figure
class Scrollable(Frame):
def __init__(self, frame, fig, width=16):
# Base class initialization
Frame.__init__(self, frame)
# Instance variable for tkinter canvas
self.tk_cnv = Canvas(frame, highlightthickness=0)
self.tk_cnv.pack(side="left", anchor="nw", fill="both",
expand=True)
# Instance variable for the scroll-bar
v_scroll = Scrollbar(frame)
v_scroll.pack(side="right", fill="y", expand=False)
v_scroll.config(command=self.tk_cnv.yview, width=width)
v_scroll.activate("slider")
# Instance variable for the matplotlib canvas
self.mpl_cnv = FigCanvas(fig, frame)
self.cnv_widget = self.mpl_cnv.get_tk_widget()
self.tk_cnv.config(yscrollcommand=v_scroll.set)
self.tk_cnv.bind("<Configure>", self.__fill_canvas)
# Assign frame generated by the class to the canvas
# and create a scrollable window for it.
self.windows_item = \
self.tk_cnv.create_window((0, 900), window=self.cnv_widget, anchor='e',
tag='self.canvas')
self.tk_cnv.config(scrollregion=self.tk_cnv.bbox("all"))
def __fill_canvas(self, event):
# Enlarge the windows item to the canvas width
canvas_width = event.width
canvas_height = event.height * 2.825
self.tk_cnv.itemconfig(self.windows_item, width=canvas_width,
height=canvas_height)
class StartPage(Frame):
""" Tkinter based class for single frame upon which widgets
such as buttons, check-buttons, and entry are used as a
simple graphical user interface.
"""
LARGE_FONT = ("Veranda", 12)
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.controller = controller
# Instance variable for third row of widgets
self.canvas_frame = Frame(self.controller, relief="sunken")
self.canvas_frame.pack(side="top", anchor="nw", fill="both",
expand=True)
# Instance variables for the figure
self.plot_fig = Figure(figsize=[14.0, 18.0])
# Instance variable for the frame with scrolling functionality
self.canvas_body = Scrollable(self.canvas_frame, self.plot_fig)
self.canvas = self.canvas_body.mpl_cnv
# Instance variable for third row of widgets
self.control_frame = Frame(self.controller, relief="sunken")
self.control_frame.pack(side="right", anchor="ne", fill="y",
expand=True)
class MainContainer(Tk):
""" Tkinter based class used to generate a single window
and contain a single frame. The frame contains multiple
widgets for user choice and visualization.
"""
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
Tk.wm_title(self, "Sequence Viewer")
Tk.wm_resizable(self, width=True, height=True)
container = Frame(self)
container.pack_configure(side="top", anchor="nw", fill="both")
self.frames = {}
frame = StartPage(container, self)
self.frames[StartPage] = frame
self.show_frame(StartPage)
self.center_window()
def show_frame(self, frame_to_add):
frame = self.frames[frame_to_add]
frame.tkraise()
def center_window(self):
w = 1100
h = 900
sw = self.winfo_screenwidth()
sh = self.winfo_screenheight()
x = (sw - w) / 2
y = (sh - h) / 2
self.geometry('%dx%d+%d+%d' % (w, h, x, y))
if __name__ == "__main__":
app = MainContainer()
app.mainloop()

Tkinter scrollbar for frame

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.

Image in corner of tkinter window in python 3

Is it possible to put a small image in tkinter window for ex. in the right corner of the window, if so how to do it?
You can create a label, put an image in that label, then use place to put it precisely where you want. For example, you can use a relative x and y of 1.0 and an anchor of "se" to put it in the lower-right-hand corner.
Here is a contrived example:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
# a simple label, just to show there's something in the frame
label = tk.Label(self, text="Example of using place")
label.pack(side="top", fill="both", expand=True)
# we'll place this image in every corner...
self.image = tk.PhotoImage(data='''
R0lGODlhEAAQALMAAAAAAP//AP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAA\nAAAAACH5BAEAAAIALAAAAAAQABAAQAQ3UMgpAKC4hm13uJnWgR
TgceZJllw4pd2Xpagq0WfeYrD7\n2i5Yb+aJyVhFHAmnazE/z4tlSq0KIgA7\n
''')
# ... by creating four label widgets ...
self.nw = tk.Label(self, image=self.image)
self.ne = tk.Label(self, image=self.image)
self.sw = tk.Label(self, image=self.image)
self.se = tk.Label(self, image=self.image)
# ... and using place as the geometry manager
self.nw.place(relx=0.0, rely=0.0, anchor="nw")
self.ne.place(relx=1.0, rely=0.0, anchor="ne")
self.sw.place(relx=0.0, rely=1.0, anchor="sw")
self.se.place(relx=1.0, rely=1.0, anchor="se")
if __name__ == "__main__":
root = tk.Tk()
root.wm_geometry("400x400")
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()

Categories