Tkinter Dynamic scrollbar for a dynamic GUI not updating with GUI - python

This is related to a previous question:
Tkinter dynamically create widgets from button
At the time that I asked the previous question, I believed that it would be easy to add a scrollable frame around the dynamic GUI. Instead, I have had a single problem with the scrollbar not detecting the new frames and entry boxes after the button is pressed. How do I solve this without editing the ScrollFrame class much?
I know that the Scrollbarframe works with other widgets it is just that the dynamic component is causing issues. When I shrink the vertical size of the window past the original location of the createWidgets button, the scrollbar appears, but the scrollbar is not present for the rest of the dynamically created widgets. Does the canvas not detect that the vertical size of the frame increases with a button press?
Note: I am aware that wildcard imports are awful. I'm just using one for the example
from tkinter import *
class AutoScrollbar(Scrollbar):
# A scrollbar that hides itself if it's not needed.
# Only works if you use the grid geometry manager!
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
# grid_remove is currently missing from Tkinter!
self.tk.call("grid", "remove", self)
else:
self.grid()
Scrollbar.set(self, lo, hi)
def pack(self, **kw):
raise TclError("cannot use pack with this widget")
def place(self, **kw):
raise TclError("cannot use place with this widget")
class ScrollFrame:
def __init__(self, master):
self.vscrollbar = AutoScrollbar(master)
self.vscrollbar.grid(row=0, column=1, sticky=N+S)
self.hscrollbar = AutoScrollbar(master, orient=HORIZONTAL)
self.hscrollbar.grid(row=1, column=0, sticky=E+W)
self.canvas = Canvas(master, yscrollcommand=self.vscrollbar.set,
xscrollcommand=self.hscrollbar.set)
self.canvas.grid(row=0, column=0, sticky=N+S+E+W)
self.vscrollbar.config(command=self.canvas.yview)
self.hscrollbar.config(command=self.canvas.xview)
# make the canvas expandable
master.grid_rowconfigure(0, weight=1)
master.grid_columnconfigure(0, weight=1)
# create frame inside canvas
self.frame = Frame(self.canvas)
self.frame.rowconfigure(1, weight=1)
self.frame.columnconfigure(1, weight=1)
def update(self):
self.canvas.create_window(0, 0, anchor=NW, window=self.frame)
self.frame.update_idletasks()
self.canvas.config(scrollregion=self.canvas.bbox("all"))
if self.frame.winfo_reqwidth() != self.canvas.winfo_width():
# update the canvas's width to fit the inner frame
self.canvas.config(width = self.frame.winfo_reqwidth())
if self.frame.winfo_reqheight() != self.canvas.winfo_height():
# update the canvas's width to fit the inner frame
self.canvas.config(height = self.frame.winfo_reqheight())
frames = []
widgets = []
def createwidgets():
global widgetNames
global frameNames
frame = Frame(o.frame, borderwidth=2, relief="groove")
frames.append(frame)
frame.pack(side="top", fill="x")
widget = Entry(frame)
widgets.append(widget)
widget.pack(side="left")
root = Tk()
o = ScrollFrame(root)
label = Label(o.frame, text = "test")
label1 = Label(o.frame, text = "test")
label2 = Label(o.frame, text = "test")
label3 = Label(o.frame, text = "test")
label.pack()
label1.pack()
label2.pack()
label3.pack()
createWidgetButton = Button(o.frame, text="createWidgets",
command=createwidgets)
createWidgetButton.pack(side="bottom", fill="x")
o.update()
root.mainloop()
This is what the window would look like if it was fully expanded
If I were to shrink the window, it should immediately create a vertical scrollbar because that would cover a widget. However, the scrollbar acts like the program was still in its initial state.
Incorrect Scrollbar(at the moment that the scrollbar appears)

You need to make sure that you update the canvas scrollregion whenever you add widgets to the inner frame. The most common solution is to bind to the frame's <Configure> event, which will fire whenever the frame changes size.
In ScrollFrame.__init__ add the following line after you create the frame:
self.frame.bind("<Configure>", self.reset_scrollregion)
Then, add this function to ScrollFrame:
def reset_scrollregion(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all")

Related

Tkinter - I created a custom scrollable frame class. Is it possible for it to automatically call a function when a child is added to it?

I make a custom tkinter widget (ScrollFrame) to make a scrollable frame using the answer from this question: Tkinter scrollbar for frame
Everything is working fine but I have to call the "ConfigureCanvas" function every time new widget is added to ScrollFrame in order to resize the scroll area. Are there any event binds that I could use that would be called when a new widget is packed/grided/placed to the Scroll Frame?
eg:
class ScrollFrame(Frame):
'''
'''
self.packFrame.bind('<NewChildWidgetAdded>', self.ConfigureCanvas)
'''
'''
exampleLabel = Label(packFrame, text='Hello')
exampleLabel.pack() # activate the "NewChildWidgetAdded" event?
Here's working code (python 3+). I created a loop that creates 50 labels to give me something to scroll over.
import tkinter
from tkinter import Tk, Frame, filedialog, Button, Listbox, Label, Entry, Text, Canvas, Scrollbar, Radiobutton, Checkbutton, Menu, IntVar, StringVar, BooleanVar, Grid, OptionMenu, Toplevel, ALL, CURRENT, END
#imported more than i needed, copied from other code where this is all used.
class ScrollFrame(Frame):
def __init__(self, frame, *args, **kwargs):
Frame.__init__(self, frame, *args, **kwargs)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.vScroll = Scrollbar(self, orient='vertical')
self.vScroll.grid(row=0, column=1, sticky='wens')
self.hScroll = Scrollbar(self, orient='horizontal')
self.hScroll.grid(row=1, column=0, sticky='wens')
self.canvas = Canvas(self, bd=0, highlightthickness=0, xscrollcommand=self.hScroll.set, yscrollcommand=self.vScroll.set, bg='green')
self.canvas.grid(row=0, column=0, sticky='wens')
self.vScroll.config(command=self.canvas.yview)
self.hScroll.config(command=self.canvas.xview)
self.packFrame = Frame(self.canvas, bg='blue')
self.packWindow = self.canvas.create_window(0,0, window=self.packFrame, anchor='nw')
def ConfigureCanvas(self):
self.packFrame.update_idletasks()
size = (self.packFrame.winfo_reqwidth(), self.packFrame.winfo_reqheight())
self.canvas.config(scrollregion=(0,0,size[0], size[1]))
mw = Tk()
scrollCavnas = ScrollFrame(mw)
scrollCavnas.pack(side='right', fill='both', expand='yes')
scrollFrame = scrollCavnas.packFrame
temp = 0
while temp < 50:
Label(scrollFrame, text=temp).grid(row=temp, column=temp)
temp += 1
scrollCavnas.ConfigureCanvas() # don't want to have to call this every time a new widget is added.
mw.mainloop()
I tried:
self.packFrame.bind('<map>', self.ConfigureCanvas)
But it looks like this only gets called when the ScrollFrame is created, not when I add a new child widget to the ScrollFrame.
I looked over the documentation (http://tcl.tk/man/tcl8.5/TkCmd/bind.htm#M13) but I didn't notice anything that could do what I wanted.
One solution is to use the Configure event. The downside is this gets called every time the widget is scrolled.
self.packFrame.bind('<Configure>', self.ConfigureCanvas)
def ConfigureCanvas(self, event=None):
self.packFrame.update_idletasks()
size = (self.packFrame.winfo_reqwidth(), self.packFrame.winfo_reqheight())
self.canvas.config(scrollregion=(0,0,size[0], size[1]))
I was able to get at least part of the function to only run when adding a new widget. This seems clunky and I believe it will only work if scrolling horizontally (it could be modified to do vertical or both though). This compares the required width to the current scroll region size and if they're different it will resize the scroll region.
def ConfigureCanvas(self, event=None):
scrollRegion = self.canvas['scrollregion'].split(' ')
try:
scrollRegion = int(scrollRegion[2])
except IndexError:
scrollRegion = None
if self.packFrame.winfo_reqwidth() != scrollRegion:
self.packFrame.update_idletasks()
size = (self.packFrame.winfo_reqwidth(), self.packFrame.winfo_reqheight())
self.canvas.config(scrollregion=(0,0,size[0], size[1]))

Tkinter Text Widget Horizontal Fill

I have a small python program to build a GUI. I'm trying to use a text widget to create an easily scrollable window that contains vertically stacked frames. A frame is created on button press and added to the bottom of the text widget. This works fine; however, I'm struggling to get these frames to stretch to fill the text box horizontally.
import Tkinter as tk
class NewEntry(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
#self.pack(fill="x", expand=True) #possible error source
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0,weight=1)
textField = tk.Entry(self)
textField.grid(row=1, column=0, padx=2, pady=1, sticky="ew")
addButton = tk.Button(self, text="Add", cursor="arrow")
addButton.grid(row=1, column=1, padx=10, pady=2, sticky="ew")
newLabel = tk.Label(self, text="Test", bg="#5522FF")
newLabel.grid(row=0, padx=2, pady=2, sticky="ew", columnspan=2)
newLabel.columnconfigure(0, weight=1)
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent)
self.parent=parent
self.grid()
self.columnconfigure(0, weight=1)
self.grid_rowconfigure(0,weight=1)
self.text = tk.Text(self, wrap="none", bg="#AA3333")
vsb = tk.Scrollbar(orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self.text.pack(fill="both", expand=True)
b = tk.Button(self, text="Button #%s" % 1, command=self.OnButtonClick)
self.text.window_create("end", window=b)
self.text.insert("end", "\n")
def OnButtonClick(self):
self.text.configure(state="normal")
panel = NewEntry(self.text, bg="#FF1111")
self.text.window_create("end", window=panel)
self.text.insert("end", "\n")
self.text.configure(state="disabled")
if __name__=="__main__":
root = tk.Tk()
root.resizable(True, True)
appinstance=MainApplication(root)
appinstance.pack(fill="both", expand=True)
root.mainloop()
I've read many different posts talking about grid_columnconfigure, fill options, sticky options, etc, but I haven't been able to get it filling properly. I am wondering if the window_create() method of the Text widget creates some sort of size limitation? It seems as though my code in NewEntry class is properly filling the space allowed by the window_create method, but I don't know how to create a "panel" to fill the width of the text box.
I am also aware of the possibility of using a canvas instead of text box (I'm wanting to maintain dynamic size and scrollability, though). I read from several different posts that a text widget is easiest if you have a simple stack of widgets, though. I will accept any recommendation, though.
The root of the problem is that a frame typically wants to fit its contents. So, when you add the label, entry widget, and button to the frame, it will shrink to fit. Any size you give to the frame will be ignored.
There are several solutions to this problem. What many people do is turn geometry propagation off (eg: self.grid_propagate(False)) but that means you have to manage both the width and height when all you really want is to control the width.
Another solution is to put something inside the panel that can be configured with an explicit width, and which will then cause the containing frame to grow to fit. For example, you can add an invisible frame in row 0 that sits behind the other widgets in row 0. When you change the width of this frame it will cause the containing frame to grow:
class NewEntry(tk.Frame):
def __init__(...):
...
# create this _before_ creating the other widgets
# so it is lowest in the stacking order
self.sizer = tk.Frame(self, height=1)
self.sizer.grid(row=0, columnspan=2)
...
With that, you can force the panel to be any width you want by setting the width of the sizer, but tkinter will continue to compute the optimum height for the widget:
self.sizer.configure(width=200)
Now you just need to set up bindings so that whenever the text widget changes size, you call this function to resize each entry.
For example, you might want to save all of the panels in a list so that you can iterate over them later.
class MainApplication(...):
def __init__(...):
...
self.panels = []
...
def OnButtonClick(...):
...
panel = NewEntry(...)
self.panels.append(panel)
...
With that, you can set up a binding that triggers whenever the window resizes:
class MainApplication(...):
def __init__(...):
...
self.text.bind("<Configure>", self.OnConfigure)
...
def OnConfigure(self, event):
for panel in self.panels:
panel.sizer.configure(width=event.width)
I wouldn't do it precisely like that since it tightly couples the implementation of the panel to the main window, but it illustrates the general technique of explicitly controlling the width of an embedded window.
Other solutions involve putting the panels inside a containing frame, and make that frame be the only widget added to the text widget. You could also use a canvas instead of a text widget since it allows you to explicitly set the width and height of embedded windows.

tkinter: scrollbar autohide without window resize

Using the folowing sample code I wrote I am having issues with some behavior.
I want to add/remove the scrollbar as needed. But when I do it shifts all other elements in the window as the window resizes. This is just a sample to demonstrate the issue, you will see the window resize when the scrollbar is added and removed. In the real application there are more widgets on the window.
Am I trying to do this the right way or if not how can I resolve the issue? I also plan to have a second widget with scrollbars as well in another separate frame.
from tkinter import *
from tkinter import ttk
class TopFrame(ttk.Frame):
def __init__(self, parent, col=0, row=0):
ttk.Frame.__init__(self, parent)
self.innerframe = ttk.Frame(parent)
self.list_scroll = ttk.Scrollbar(self.innerframe)
self.list_scroll.grid(column=1, row=0, sticky=NS)
self.list_scroll.grid_remove()
self.list = Listbox(self.innerframe, width=64, height=8,
yscrollcommand=self.list_scroll.set)
self.list_scroll.config(command=self.list.yview)
self.list.grid(column=0, row=0, sticky=NSEW)
self.innerframe.grid(column=col, row=row)
self.addbtn = ttk.Button(parent, text='add item',
command=self.additem)
self.addbtn.grid(column=col, row=row+1, padx=10, pady=2)
self.delbtn = ttk.Button(parent, text='del item',
command=self.delitem)
self.delbtn.grid(column=col, row=row+2, padx=10, pady=2)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
def additem(self):
count = str(len(self.list.get(0, END)))
self.list.insert(END, 'demo' + count)
if len(self.list.get(0, END)) > 8:
self.list_scroll.grid()
def delitem(self):
self.list.delete(END)
if len(self.list.get(0, END)) <= 8:
self.list_scroll.grid_remove()
class MasterFrame(Tk):
def __init__(self):
Tk.__init__(self)
topframe = TopFrame(self)
if __name__ == '__main__':
MasterFrame().mainloop()
Once the window has been displayed for the first time you can get the window size, and then use that to call the geometry method on the root window. When you set the size of the window with the geometry command it will stop resizing based on changes to its internal widgets.
The simplest thing is to write a function to do that, and schedule it to run with after_idle, which should fire after the window is first displayed.

Dynamic Button with ScrollBar in tkinter - Python

I had an requirement for creating dynamic buttons in tkinter window,But i tried Scroll bar option which is not helping me to scroll the buttons in the tkinter window,Is any other option to scroll the Dynamic buttons.
Code:
root = tkinter.Tk()
root.title("Links-Shortcut")
root.configure(background="gray99")
sw= tkinter.Scrollbar(root)
sw.pack(side=RIGHT, fill=Y)
os.chdir("C:\Bo_Link")
with open('Bo_ol_links.csv', 'r', newline='') as fo:
lis=[line.strip('\r\n').split(',') for line in fo] # create a list of lists
lis=sorted(lis)
#print (lis)
for i,x in enumerate(lis):
btn = tkinter.Button(root,height=1, width=20,relief=tkinter.FLAT,bg="gray99",fg="purple3",font="Dosis",text=lis[i][0],command=lambda i=i,x=x: openlink(i))
btn.pack(padx=10,pady=5,side=tkinter.TOP)
def openlink(i):
os.startfile(lis[i][1])
root.mainloop()
Thanks.
This code packs buttons into a scrollable Frame that I stole from found at the Tkinter Unpythonic Wiki. I'm running it on Python 2, so I use Tkinter as the module name in the import statement, for Python 3 change that statement to use tkinter.
import Tkinter as tk
class VerticalScrolledFrame(tk.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):
tk.Frame.__init__(self, parent, *args, **kw)
# create a canvas object and a vertical scrollbar for scrolling it
vscrollbar = tk.Scrollbar(self, orient=tk.VERTICAL)
vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=tk.FALSE)
canvas = tk.Canvas(self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.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 = tk.Frame(canvas)
interior_id = canvas.create_window(0, 0, window=interior,
anchor=tk.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)
root = tk.Tk()
root.title("Scrollable Frame Demo")
root.configure(background="gray99")
scframe = VerticalScrolledFrame(root)
scframe.pack()
lis = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
for i, x in enumerate(lis):
btn = tk.Button(scframe.interior, height=1, width=20, relief=tk.FLAT,
bg="gray99", fg="purple3",
font="Dosis", text='Button ' + lis[i],
command=lambda i=i,x=x: openlink(i))
btn.pack(padx=10, pady=5, side=tk.TOP)
def openlink(i):
print lis[i]
root.mainloop()
Believe it or not, the simplest solution for a vertical stack of buttons might be to add the buttons to a text widget. You can do the frame-in-a-canvas solution which gives a lot of flexibility, but it's a bit more work. Using a text widget as a container doesn't give much flexibility with respect to layout, but it's very easy if all you need is a vertical stack of widgets.
Here is a working example:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
text = tk.Text(self, wrap="none")
vsb = tk.Scrollbar(orient="vertical", command=text.yview)
text.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
text.pack(fill="both", expand=True)
for i in range(20):
b = tk.Button(self, text="Button #%s" % i)
text.window_create("end", window=b)
text.insert("end", "\n")
text.configure(state="disabled")
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.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.

Categories