Unexpected Grid behavior in Tkinter - python

I am coding a game in Python with tkinter. The basic functionality is that an image is displayed but it's covered with a grid of boxes that can be destroyed. I've written a linear version of the code that is as simplified as I can get it:
from tkinter import *
from PIL import ImageTk, Image
class Box:
def __init__(self, parent, row, column, width, height):
self.row = row
self.column = column
self.canvas = Canvas(parent, bg='black', width=width, height=height)
self.canvas.grid(row=row, column=column, sticky=NSEW)
self.canvas.bind('<Button-1>', self.destroy)
def destroy(self, event):
print('Destroyed box at', self.row, self.column)
self.canvas.destroy()
root = Tk()
root.title('Image Reveal')
root.state('zoomed')
image_raw = None
# Create a frame for the image
image_frame = Frame(root, highlightbackground="blue", highlightthickness=5)
image_frame.pack(expand=True, anchor=CENTER, fill=BOTH, padx=0, pady=0)
# Load image and put it in a label
image_raw = Image.open('image.png')
image = ImageTk.PhotoImage(image_raw)
image_label = Label(image_frame, image=image, borderwidth=5, relief='ridge')
image_label.pack(anchor=CENTER, expand=True)
# Calculate the size of the boxes
image_label.update()
width = image.width()
height = image.height()
column_amount = 10
row_amount = 10
box_width = width / column_amount
box_height = height / row_amount
# Create boxes that fill the grid
for row in range(row_amount):
for column in range(column_amount):
Box(image_label, row, column, box_width, box_height)
root.mainloop()
You can click on the box to destroy it and it also logs the coordinates of the box destroyed.
The desired behavior: You can click on the boxes to destroy them, gradually revealing the image. The other boxes should stay in the same place (the same cells of the grid), so the grid shape doesn't change.
The unexpected behavior: Destroying one box makes the rest of the boxes disappear somehow. Yet you can still click them (it still logs the destruction message). Overall this is the closest I've got to my desired behavior but I can't figure out why this is happening or how to approach this functionality better.
WORKING CODE:
Lukas Krahbichler helped achieve most of the desired behavior. Here is the updated code.
Destroying the boxes however led to the grid resizing when a full row or a column of boxes gets destroyed. In order to fix that, I instead hide the box by lowering it behind the image. At first that didn't work, but then I found this -
Turns out to lower the WHOLE canvas, there is no direct method for that. The final code:
from tkinter import *
from PIL import ImageTk, Image
class Box:
def __init__(self, root, image_frame, image_label, row, column):
self.root = root
self.row = row
self.column = column
self.image_label = image_label
self.image_frame = image_frame
self.visible = True
self.canvas = Canvas(image_frame, bg='black')
self.canvas.grid(row=row, column=column, sticky=NSEW, padx=5, pady=5)
self.canvas.bind('<Button-1>', self.hide)
def hide(self, event):
# Lower the box under the image to hide it
self.canvas.tk.call('lower', self.canvas._w, None)
self.visible = False
print('Hid box at', self.row, self.column)
root = Tk()
root.title('Image Reveal')
root.state('zoomed')
image_raw = None
column_amount = 10
row_amount = 10
boxes = []
# Create a frame for the image
image_frame = Frame(root, highlightbackground="blue", highlightthickness=0)
image_frame.pack(anchor=CENTER, fill=None, padx=0, pady=0)
# Load image and put it in a label
image_raw = Image.open('image.png')
image = ImageTk.PhotoImage(image_raw)
width = image.width()
height = image.height()
# Create a label for the image
image_label = Label(image_frame, image=image, borderwidth=0, relief='ridge')
# Configure the image frame so it doesn't resize <----- UPDATED
image_frame.config(width=width, height=height)
image_frame.grid_propagate(False)
image_label.grid_propagate(False)
# Place the image label in the frame
image_label.grid(rowspan=row_amount, columnspan=column_amount, sticky="NSEW")
# Configure the grid weights <----- UPDATED
for row in range(10):
image_frame.rowconfigure(row, weight=1)
for column in range(10):
image_frame.columnconfigure(column, weight=1)
# Calculate the size of the boxes
image_label.update()
# Create boxes that fill the grid
for row in range(row_amount):
for column in range(column_amount):
box = Box(root, image_frame, image_label, row, column)
boxes.append(box)
root.mainloop()

In tkinter you are not really supposed to place other widgets (for a example a Canvas) in a Label. If you grid everything in the "image_frame" and give the "image_label" a column and a rowspan it should work.
from tkinter import *
from PIL import ImageTk, Image
class Box:
def __init__(self, parent, row, column, width, height):
self.row = row
self.column = column
self.canvas = Canvas(parent, bg='black', width=width, height=height)
self.canvas.grid(row=row, column=column, sticky=NSEW)
self.canvas.bind('<Button-1>', self.destroy)
def destroy(self, event):
print('Destroyed box at', self.row, self.column)
self.canvas.destroy()
root = Tk()
root.title('Image Reveal')
root.state('zoomed')
image_raw = None
# Create a frame for the image
image_frame = Frame(root, highlightbackground="blue", highlightthickness=5)
image_frame.pack(expand=True, anchor=CENTER, fill=BOTH, padx=0, pady=0)
# Define row and column amount
column_amount = 10
row_amount = 10
# Load image and put it in a label
image_raw = Image.open('image.png')
image = ImageTk.PhotoImage(image_raw)
image_label = Label(image_frame, image=image, borderwidth=5, relief='ridge')
image_label.grid(rowspan=row_amount, columnspan=column_amount, sticky="NSEW")
# Calculate the size of the boxes
image_label.update()
width = image.width()
height = image.height()
box_width = width / column_amount
box_height = height / row_amount
# Create boxes that fill the grid
for row in range(row_amount):
for column in range(column_amount):
Box(image_frame, row, column, box_width, box_height)
root.mainloop()

Related

Tkinter widgets in a notebook tab only showing when mouse is moved off of the tab label

I recently made 2 functions, add_scrollbar and update_scroll_region, which adds a scrollbar to a given frame and updates the scroll region when widgets in that frame change.
The frame in which I am adding a scrollbar is a notebook tab. The functions work as intended, but when I switch off a tab with the scrollbar (and only the ones with the scrollbar) and come back to it, all the widgets are gone. If I move my mouse off of the tab label, all the widgets re-appear. I am not exactly sure what it is about these 2 functions that could be causing this kind of behavior. I have provided a simplified example below. In this example, TAB1 has the scrollbar, and TAB2 does not. You will notice that switching from TAB2 to TAB1 hides the button in TAB1 until the mouse is moved.
from tkinter import *
from tkinter import ttk
root = Tk()
root.geometry("1200x1200")
def add_scrollbar(outer_frame):
canvas = Canvas(outer_frame)
canvas.pack(side=LEFT, fill=BOTH, expand=1)
information_frame = Frame(canvas)
canvas.create_window((0, 0), window=information_frame)
scrollbar = ttk.Scrollbar(outer_frame, orient=VERTICAL, command=canvas.yview)
scrollbar.pack(side=RIGHT, fill=Y)
canvas.configure(yscrollcommand=scrollbar.set)
return information_frame, canvas
def update_scroll_region(canvas):
global root
root.update()
bbox = canvas.bbox("all")
x, y, width, height = bbox
if height < canvas.winfo_height():
bbox = x, y, width, canvas.winfo_height()
canvas.configure(scrollregion=bbox)
return
def create_example():
global root
notebook = ttk.Notebook(root, height=1200, width=1500)
notebook.pack(pady=10)
my_outer_frame_1 = Frame(root)
my_outer_frame_1.pack(fill=BOTH, expand=1)
notebook.add(my_outer_frame_1, text="TAB1")
inner_frame_1, my_canvas_1 = add_scrollbar(my_outer_frame_1)
my_outer_frame_2 = Frame(root)
my_outer_frame_2.pack(fill=BOTH, expand=1)
notebook.add(my_outer_frame_2, text="TAB2")
Label(my_outer_frame_2, text="This always shows").pack()
# ^^^ Sets up a notebook with 2 tabs
changing_frame = Frame(inner_frame_1, borderwidth=4) # this is the frame that will be changing its contents
changing_frame.pack(side=LEFT, anchor="n")
display_frame(changing_frame, my_outer_frame_1, my_canvas_1)
# this method re-displays the changing frame depending on the specified size ('big' or 'small')
root.mainloop()
return
def display_frame(frame, outer_frame, canvas, size='small'):
for widget in frame.winfo_children():
widget.destroy()
if size == 'small':
Button(frame, text="This button is gone until the mouse is moved",
command=lambda this_frame=frame: display_frame(this_frame, outer_frame, canvas, size='big')).grid(row=0,
column=0)
elif size == 'big':
Button(frame, height=5, width=5, text="Hide",
command=lambda this_frame=frame: display_frame(this_frame, outer_frame, canvas, size='small')).grid(
row=0, column=0)
for n in range(1, 100):
Label(frame, text="Other Stuff!").grid(row=n, column=0)
update_scroll_region(canvas)
return
if __name__ == '__main__':
create_example()

Tkinter,Canvas converting/saving details

here is my problem. I'm trying to convert my canvas drew into an image. First of all, I create the postscript of my canvas drew, then I convert it into png. The problem is that I obtain white images w/o changes.
So,there is my class:
class Paint(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.color = "white"
self.brush_size = 5
self.setUI()
def draw(self, event):
self.canv.create_rectangle(event.x - 7, event.y - 7, event.x + 7, event.y + 7,
fill=self.color, outline=self.color)
def setUI(self):
self.pack(fill=BOTH, expand=1)
self.canv = Canvas(self, bg="black")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.canv.grid(padx=5, pady=5, sticky=E + W + S + N)
self.canv.bind("<B1-Motion>", self.draw)
def clear(self):
self.canv.delete(ALL)
def save(self):
self.canv.update()
self.canv.postscript(file="D:\\Новая папка\\test.xps", colormode="color")
im2 = Image.open("D:\\Новая папка\\test.xps")
im2.save("D:\\Новая папка\\test.png", 'png')
My main:
root = Tk()
frame=Canvas(root,height=200,width=300)
root.geometry("500x600")
app = Paint(frame)
frame.create_rectangle(10,10,50,50)
frame.pack()
b3= Button(
text="Apply!",
width=35,
height=1,
bg="white",
fg="black",
command=lambda :[which_button("Activated"),b3_clicked(),app.save()],
font=25
)
b3.pack(side=TOP)
root.mainloop()
When you save the canvas drawings to postscript file, the background color of the canvas is ignored. Since you have used white color as the paint color, so the output drawings are white on white which looks like nothing is drawn.
If you want to have a black background in the output postscript file, use .create_rectangle(..., fill="black") to fill the canvas with black color.
class Paint(Frame):
...
def setUI(self):
# better call pack() outside Paint class
#self.pack(fill=BOTH, expand=1)
self.canv = Canvas(self)
# fill the canvas with black color
# make sure the rectangle is big enough to cover the visible area
self.canv.create_rectangle(0, 0, 1000, 1000, fill="black")
...
Also below is how you should create the Paint class:
root = Tk()
root.geometry("500x600")
app = Paint(root)
app.pack(fill=BOTH, expand=1)
...

Canvas background image doesnt fit the entire window tkinter python

I'm new with tkinter GUI and trying to make a background to a top-level window by using canvas. I have tried to make the canvas the same size as the entire window by expand=TRUE.
However, it doesn't work well and the image is not sized as the window. can anyone help me fix that problem?
The top-level window is overviewWindow, which is in the function createOverviewWindow.
This is my code:
There is no output image because unfortunately I cant add images. The window I got has the background image in the top left corner but small sized
from tkinter import *
from tkinter import ttk, font, messagebox
from PIL import ImageTk, Image
import os
root = Tk()
root.title("Decoder of ultrasound images to detect colon tumors")
# Adding window icon
root.iconbitmap('afekaImage.ico')
rootWidth, rootHeight = 600, 600
screenWidth = root.winfo_screenwidth()
screenHeight = root.winfo_screenheight()
topLeftPosition = (screenWidth / 2 - rootWidth / 2, screenHeight / 2 - rootHeight / 2)
# Configure window size
root.geometry(f'{rootWidth}x{rootHeight}+{int(topLeftPosition[0])}+{int(topLeftPosition[1])}')
# open doc file
def openDocFile():
os.startfile("FinalProject.docx")
# adjusting background image to fit window
def adjustBackgroundImage(event):
label = event.widget
# avoid garbage collection option 1
# global resizedBackgroundImage, newBackgroundImage
# ----------
width = event.width
height = event.height
resizedBackgroundImage = copyImage.resize((width, height))
newBackgroundImage = ImageTk.PhotoImage(resizedBackgroundImage)
label.config(image=newBackgroundImage)
# avoid garbage collection option 2
label.image = newBackgroundImage
# ----------
def createUserManualWindow(button_userManual):
# global image1
userManualWindow = Toplevel(root)
userManualWindow.geometry(f'{rootWidth}x{rootHeight}+{int(topLeftPosition[0])}+{int(topLeftPosition[1])}')
userManualWindow.iconbitmap('afekaImage.ico')
image1 = ImageTk.PhotoImage(Image.open('background.jpg'))
label1 = ttk.Label(userManualWindow, image=image1)
label1.pack()
label1.bind('<Configure>', adjustBackgroundImage)
label1.pack(fill=BOTH, expand=YES)
def activateButtonUserManual():
button_userManual.configure(state="normal")
userManualWindow.destroy()
button_userManual.configure(state="disabled")
button_exit_userManualWindow = Button(userManualWindow, text="Exit", font=fontStyle,
command=lambda: [userManualWindow.destroy(), activateButtonUserManual()])
button_exit_userManualWindow.place(relx=0.4, rely=0.8, relwidth=0.2, relheight=0.1)
# will occurs only when esc pressed
userManualWindow.protocol("WM_DELETE_WINDOW", activateButtonUserManual)
# ----------
def createOverviewWindow(button_overview):
global image1, canvas
overviewWindow = Toplevel(root)
overviewWindow.geometry(f'{rootWidth}x{rootHeight}+{int(topLeftPosition[0])}+{int(topLeftPosition[1])}')
overviewWindow.iconbitmap('afekaImage.ico')
canvas = Canvas(overviewWindow, width=600, height=600)
canvas.pack(fill=BOTH, expand=TRUE)
image1 = ImageTk.PhotoImage(Image.open('background.jpg'))
canvas.create_image(0, 0, image=image1, anchor='nw')
image = Image.open('background.jpg')
copyImage = image.copy()
backgroundImage = ImageTk.PhotoImage(image)
label = ttk.Label(root, image=backgroundImage)
label.bind('<Configure>', adjustBackgroundImage)
label.pack(fill=BOTH, expand=YES)
fontStyle = font.Font(family="Segoe Script", size=10, weight=font.BOLD)
# Create buttons
button_userManual = Button(root, text="USER MANUAL", command=lambda: createUserManualWindow(button_userManual), font=fontStyle)
button_userManual.place(relx=0.4, rely=0.2, relwidth=0.2, relheight=0.1)
button_overview = Button(root, text="OVERVIEW", command=lambda: createOverviewWindow(button_overview), font=fontStyle)
button_overview.place(relx=0.4, rely=0.4, relwidth=0.2, relheight=0.1)
button_openDocFile = Button(root, text="DOC FILE", font=fontStyle, command=openDocFile)
button_openDocFile.place(relx=0.4, rely=0.6, relwidth=0.2, relheight=0.1)
button_quit = Button(root, text="Exit", font=fontStyle, command=root.destroy)
button_quit.place(relx=0.4, rely=0.8, relwidth=0.2, relheight=0.1)
root.mainloop()
The image you put was too big for the canvas. It's like putting a 8K image into a 720p monitor. It simply does not fit. So the solutions are
a) resize the image so it fits in the canvas (I see you have the PIL module so it should be quick)
b) Change the canvas size so it fits the image

trying to freeze the labels only while moving horizontally (not while moving vertically) in tkinter GUI

There is a master window named top_wl. And frame_wl is created in it.
I have set of labels that are created in a frame_wl (which is on a canvas_wl) and also I am creating another small frame (fram_s) in master window (top_wl) which is on the separate canvas( canvas_s). another frame is created in the canvas_s ( named frame_s1). And i have created one label on fram_s( small frame). have set one horizontal scroll bar for canvas_wl alone .I have set both the canvas_wl and canvas_s on single vertical scroll bar (named : scroll_bar_v). The idea behind this is to freeze the labels in the small frame while moving horizontally where as it has moved with vertical scroll bar.
Now the problem is the labels on the small frame ( fram_s) is freezed on both vertical and horizontal movement. can some body help me to solve the problem? or please tell me where I am going wrong ?
top_wl = tk.Tk()# tk.Tk()
top_wl.title("Work Shop Layout")
fram_wl =tk.Frame(top_wl,width=1500,height=5000,bg = 'khaki1',relief= SUNKEN)
# Creating small frame
fram_s = tk.Frame(top_wl,width = 300,height = 500,bg = 'sky blue')
fram_s.place( x = 15, y = 90)
canvas_s = tk.Canvas(fram_s)
frame_s1 = tk.Frame(canvas_s)
l_12 = tk.Label(fram_s, text = 'Hello There',bd = 2, width = 30,bg = 'white',font = ('Arial',10,'bold'),relief = "solid")
l_12. place(x = 10, y = 30)
# finished samll frame
canvas_wl = tk.Canvas(fram_wl,width=1500,height=4000,bg = 'lime green')
# top frame portion
scroll_bar_v = tk.Scrollbar(fram_wl,orient='vertical')
scroll_bar_v.config(command = canvas_wl.yview )
scroll_bar_h = Scrollbar(fram_wl,orient='horizontal')
scroll_bar_h.config(command = canvas_wl.xview)
scroll_bar_h.pack( side = BOTTOM, fill = X )
t1 = tk.Label(fram_wl)#, text = "Hello There: Number_1",width = 50,height = 100,bg = 'Sky blue',font = ('Arial',10,'bold'),relief = "sunken") #(x = 710, y = 50)
scroll_bar_v.pack( side = RIGHT, fill = Y )
t1.pack(side = RIGHT)
scrollable_frame_wl = tk.Frame(canvas_wl,bg = 'moccasin')#, bg = 'cyan2')
canvas_wl.create_window(0, 0, window=scrollable_frame_wl, anchor='nw')
left_wl = tk.Frame(scrollable_frame_wl, borderwidth=2, height = 5000,width = 10000,relief="sunken") #
left_wl. pack(side = 'left',padx=10, pady=10)
left_wl.pack_propagate(0)
scrollable_frame_wl.update()
canvas_s.configure(yscrollcommand = scroll_bar_h.set,scrollregion = canvas_s.bbox("all"))
canvas_s.create_window((0,0),window =frame_s1, anchor = 'nw' )
canvas_wl.configure(xscrollcommand= scroll_bar_h.set,yscrollcommand= scroll_bar_v.set,scrollregion=canvas_wl.bbox("all"))
canvas_wl.pack(fill = 'both')
fram_wl.pack()
top_wl.mainloop()
This code scroll vertically both canvas and horizontally only bigger canvas.
There was mess in code so I organized code in different way to make some order.
First create scrollbars (which will be used by both canvas)
Next I create bigger frame with canvas and inner frame.
Next in the same way I create smaller frame with canvas and inner frame.
Next I add some elements to inner frames - in both I add label at the top and at the bottom of inner frame. This way I will see if it scroll to the end.
And finally I assign scrollbars to canvas to move them. Next I assign canvas to scrollbars to resize scrollbars to correct size.
And it works
There is only one problem. At this moment both frames shows only 1/3 of canvas height - so vertical scrollbar need to show slider with the same size (1/3 height) - but when you resize window then bigger frame show more canvas and scrollbar also show longer slider but smaller frame still show 1/3 of canvas and it would need smaller slider - and it would make conflict. It would display longer slider for short time (for bigger frame) and next longer slider for longer time (for shorter frame). Resizing also makes problem when you scroll down - first it shows bottom label in bigger frame but it still need mouse move to show bottom label in smaller frame.
I don't see good solution for this conflict - eventually you would have to resize smaller frame to show the same proporiton of canvas.
import tkinter as tk
top_wl = tk.Tk()
top_wl.title("Work Shop Layout")
# --- scrolls outside big frame (directly in window) ---
scroll_bar_v = tk.Scrollbar(top_wl, orient='vertical')
scroll_bar_v.pack(side='right', fill='y')
scroll_bar_h = tk.Scrollbar(top_wl, orient='horizontal')
scroll_bar_h.pack(side='bottom', fill='x')
# --- big frame (directly in window) ---
# external frame
fram_wl = tk.Frame(top_wl, width=500, height=500, bg='khaki1', relief='sunken')
fram_wl.pack(side='right', expand=True, fill='both')
fram_wl.pack_propagate(False) # don't resize external frame to canvas size
# inne canvas with size bigger then external frame
canvas_wl = tk.Canvas(fram_wl, width=1500, height=1500, bg='lime green')
canvas_wl.pack(fill='both')
# inner frame with size bigger then frame (scrollbars will use this size to scroll it)
scrollable_frame_wl = tk.Frame(canvas_wl, width=1500, height=1500, bg='moccasin')#, bg = 'cyan2')
canvas_wl.create_window(0, 0, window=scrollable_frame_wl, anchor='nw')
# --- small frame (inside big frame so it doesn't hide scrollbars when window is smaller) ---
# external frame
#fram_s = tk.Frame(top_wl, width=300, height=300, bg='sky blue') # when it is directly in top_wl then it may hide scrollbars when window is smaller
fram_s = tk.Frame(fram_wl, width=300, height=300, bg='sky blue')
fram_s.place(x=0, y=100)
fram_s.pack_propagate(False) # don't resize external frame to canvas size
# inne canvas with size bigger then external frame
canvas_s = tk.Canvas(fram_s, width=900, height=900, bg='blue')
canvas_s.pack()
# inner frame with size bigger then frame (scrollbars will use this size to scroll it)
scrollable_frame_s = tk.Frame(canvas_s, width=900, height=900, bg='red')#, bg = 'cyan2')
canvas_s.create_window(0, 0, window=scrollable_frame_s, anchor='nw')
# ---
# add to inner frame in frame WL
label_wl_1 = tk.Label(scrollable_frame_wl, text='WL-Frame Top', bd=2, width=30, bg='white', font=('Arial', 10, 'bold'), relief="solid")
label_wl_1.place(x=10, y=30)
label_wl_2 = tk.Label(scrollable_frame_wl, text='WL-Frame Bottom', bd=2, width=30, bg='white', font=('Arial', 10, 'bold'), relief="solid")
label_wl_2.place(x=10, y=1500-30)
#label_wl_1.pack()
# add to inner frame in frame S
label_s_1 = tk.Label(scrollable_frame_s, text='S-Frame Top', bd=2, width=30, bg='white', font=('Arial', 10, 'bold'), relief="solid")
label_s_1.place(x=10, y=30)
label_s_2 = tk.Label(scrollable_frame_s, text='S-Frame Bottom', bd=2, width=30, bg='white', font=('Arial', 10, 'bold'), relief="solid")
label_s_2.place(x=10, y=900-30)
#label_s_1.pack()
# --- set scrollbars to move canvas ---
# to scroll vertically both canvas
def scroll_two_canvas(*args):
#print(args)
canvas_s.yview(*args)
canvas_wl.yview(*args)
scroll_bar_v.config(command=scroll_two_canvas) # it has to scroll two canvas
# to scroll horizontally one one canvas
scroll_bar_h.config(command=canvas_wl.xview) # it scroll only one canvas
# --- set canvas to resize scrollbars ---
# to update scrollbars when canvas is moved
canvas_wl.configure(xscrollcommand=scroll_bar_h.set)
canvas_wl.configure(yscrollcommand=scroll_bar_v.set)
# do't use it because it will conflict with previous
#canvas_s.configure(yscrollcommand=scroll_bar_v.set)
# get current size of element on canvas - if elements on canvas may change size (inner frame can change size) then you may need `bind('<Configure>')`
canvas_wl.configure(scrollregion=canvas_wl.bbox("all"))
canvas_s.configure(scrollregion=canvas_s.bbox("all"))
#def update_config(event):
# print(event)
# canvas_wl.configure(scrollregion=canvas_wl.bbox("all"))
# canvas_s.configure(scrollregion=canvas_s.bbox("all"))
#top_wl.bind('<Configure>', update_config)
# ---
top_wl.mainloop()
EDIT:
Code which resize smaller frame when bigger frame changes height. This way both frames shows the same percent of canvas height and scrollbar works better.
import tkinter as tk
top_wl = tk.Tk()
top_wl.title("Work Shop Layout")
# --- scrolls outside big frame (directly in window) ---
scroll_bar_v = tk.Scrollbar(top_wl, orient='vertical')
scroll_bar_v.pack(side='right', fill='y')
scroll_bar_h = tk.Scrollbar(top_wl, orient='horizontal')
scroll_bar_h.pack(side='bottom', fill='x')
# --- big frame (directly in window) ---
# external frame
fram_wl = tk.Frame(top_wl, width=500, height=500, bg='khaki1', relief='sunken')
fram_wl.pack(side='right', expand=True, fill='both')
fram_wl.pack_propagate(False) # don't resize external frame to canvas size
# inne canvas with size bigger then external frame
canvas_wl = tk.Canvas(fram_wl, width=1500, height=1500, bg='lime green')
canvas_wl.pack(fill='both')
# inner frame with size bigger then frame (scrollbars will use this size to scroll it)
scrollable_frame_wl = tk.Frame(canvas_wl, width=1500, height=1500, bg='moccasin')#, bg = 'cyan2')
canvas_wl.create_window(0, 0, window=scrollable_frame_wl, anchor='nw')
# --- small frame (inside big frame so it doesn't hide scrollbars when window is smaller) ---
# external frame
#fram_s = tk.Frame(top_wl, width=300, height=300, bg='sky blue') # when it is directly in top_wl then it may hide scrollbars when window is smaller
fram_s = tk.Frame(fram_wl, width=300, height=300, bg='sky blue')
fram_s.place(x=0, y=100)
fram_s.pack_propagate(False) # don't resize external frame to canvas size
# inne canvas with size bigger then external frame
canvas_s = tk.Canvas(fram_s, width=900, height=900, bg='blue')
canvas_s.pack()
# inner frame with size bigger then frame (scrollbars will use this size to scroll it)
scrollable_frame_s = tk.Frame(canvas_s, width=900, height=900, bg='red')#, bg = 'cyan2')
canvas_s.create_window(0, 0, window=scrollable_frame_s, anchor='nw')
# ---
# add to inner frame in frame WL
label_wl_1 = tk.Label(scrollable_frame_wl, text='WL-Frame Top', bd=2, width=30, bg='white', font=('Arial', 10, 'bold'), relief="solid")
label_wl_1.place(x=10, y=30)
label_wl_2 = tk.Label(scrollable_frame_wl, text='WL-Frame Bottom', bd=2, width=30, bg='white', font=('Arial', 10, 'bold'), relief="solid")
label_wl_2.place(x=10, y=1500-30)
#label_wl_1.pack()
# add to inner frame in frame S
label_s_1 = tk.Label(scrollable_frame_s, text='S-Frame Top', bd=2, width=30, bg='white', font=('Arial', 10, 'bold'), relief="solid")
label_s_1.place(x=10, y=30)
label_s_2 = tk.Label(scrollable_frame_s, text='S-Frame Bottom', bd=2, width=30, bg='white', font=('Arial', 10, 'bold'), relief="solid")
label_s_2.place(x=10, y=900-30)
#label_s_1.pack()
# --- set scrollbars to move canvas ---
# to scroll both canvas
def scroll_two_canvas(*args):
#print(args)
canvas_s.yview(*args)
canvas_wl.yview(*args)
scroll_bar_v.config(command=scroll_two_canvas) # it has to scroll two canvas
scroll_bar_h.config(command=canvas_wl.xview) # it scroll only one canvas
# --- set canvas to resize scrollbars ---
# to update scrollbars when canvas is moved
canvas_wl.configure(xscrollcommand=scroll_bar_h.set)
canvas_wl.configure(yscrollcommand=scroll_bar_v.set)
# do't use it because it will conflict with previous
#canvas_s.configure(yscrollcommand=scroll_bar_v.set)
# get current size of element on canvas - if elements on canvas may change size (inner frame can change size) then you may need `bind('<Configure>')`
canvas_wl.configure(scrollregion=canvas_wl.bbox("all"))
canvas_s.configure(scrollregion=canvas_s.bbox("all"))
# --- resize smaller frame when bigger frame changes height
# to keep current height and compare with new height when resize, and to calculate scale used to resize smaller framer
frame_wl_height = fram_wl['height']
def update_config(event):
global frame_wl_height
if event.widget == fram_wl: #'.':
scale = event.height/frame_wl_height
frame_wl_height = event.height
fram_s['height'] = fram_s['height'] * scale
#canvas_wl.configure(scrollregion=canvas_wl.bbox("all"))
#canvas_s.configure(scrollregion=canvas_s.bbox("all"))
top_wl.bind('<Configure>', update_config)
# ---
top_wl.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